Movatterモバイル変換


[0]ホーム

URL:


Logo

目次

キーワード

概要

定数」で、読み取り専用のフィールドが作れるという話をしました。この時点ではまだクラス構造体値型と参照型の違いなどについて触れていなかったのでreadonly修飾子の簡単な紹介だけに留めましたが、本項で改めてreadonlyについて説明します。

整数などの基本的な型に対して使う分には特に問題は起きないんですが、構造体やクラスなど、複合型に対して使うときには注意が必要です。

参照型のフィールドに対して readonly

readonlyに関して最も注意が必要な点は、readonlyは再帰的には働かないという点です。readonlyを付けたその場所だけが読み取り専用になり、参照先などについては書き換えが可能です。

例えば以下のコードを見てください。Programクラスのフィールドcにはreadonlyが付いていますが、cが普通に書き換え可能なクラスのフィールドなので、クラスの中身は自由に書き換えられます。

// 書き換え可能なクラスclassMutableClass{// フィールドを直接公開publicint X;// 書き換え可能なプロパティpublicint Y {get;set; }// フィールドの値を書き換えるメソッドpublicvoid M(int value) => X = value;}classProgram{staticreadonlyMutableClass c =newMutableClass();staticvoid Main()    {// これは許されない。c は readonly なので、c 自体の書き換えはできないc =newMutableClass();// けども、c の中身までは保証してない// 書き換え放題        c.X = 1;        c.Y = 2;        c.M(3);    }}

参照型のフィールドに対してreadonlyを付ける例

クラスを書き換えできないように作る場合、クラス自体を書き換え不能に作りましょう。(クラスの方で、フィールドをreadonlyにしたり、プロパティをget-onlyにします。)

値型のフィールドに対して readonly

クラス(参照型)とは対照的に、構造体(値型)の場合はデータを直接持ちます。そのため、構造体のフィールドに対してreadonlyを付けると、構造体の中身も読み取り専用になります。ただし、メソッドの呼び出しなどを行う際、コピーが発生するという別の注意が必要です。

例えば以下のように、readonlyが付いたフィールドc自体に加えて、cのフィールドも書き換えできません。

using System;// 書き換え可能な構造体structMutableStruct{// フィールドを直接公開publicint X;// フィールドの値を書き換えるメソッドpublicvoid M(int value) => X = value;}classProgram{staticreadonlyMutableStruct c =newMutableStruct();staticvoid Main() => Allowed();privatestaticvoid NotAllowed()    {// これはもちろん許されない。c は readonly なので、c 自体の書き換えはできないc =newMutableStruct();// 構造体の場合、フィールドに関しては readonly な性質を引き継ぐc.X = 1;    }privatestaticvoid Allowed()    {// でも、メソッドは呼べてしまう        c.M(3);// X を 3 で上書きしているはず?Console.WriteLine(c.X);// でも、X は 0 のまま//↑のコードは、実はコピーが発生している// 以下のコードと同じ意味になるvar local = c;        local.M(3);Console.WriteLine(c.X);// 書き換わってるのは local (コピー)の方なので、c は書き換わらない(0)Console.WriteLine(local.X);// もちろんこっちは書き換わってる(3)    }}

値型のフィールドに対してreadonlyを付ける例

この例の後半を見ての通り、メソッドは呼べてしまいます。フィールドXは書き換えれないはずなのに、そのXを書き換えているメソッドMを呼んでもエラーになりません。C# では、こういう場合に、readonlyであることを保証しつつメソッドを呼び出せるように、フィールドを一度コピーしてから、そのコピーに対してメソッドを呼ぶということをしています。

このコピーは、万が一に備えて防衛的にコピー(defensive copy)するものです。実際にコピーが必要かどうか(実際にメソッド内で書き換えをしているかどうか)に関わらず、常にコピーが発生します。ソースコード上は目に見えないコピーなので、隠れたコピー(hidden copy)と呼ばれたりもします。

すなわち、コピーが発生してまずいような場合(例えば構造体のサイズが大きくてコピーにコストが掛かるとか)には、readonlyなフィールドを使うことで問題が発生することがあります。この問題は、in引数などでも発生しまえます。後述するreadonly structreadonly 関数メンバーを使えばこの問題は少し緩和するので、そちらも参照してください。

構造体の this 書き換え

C# のreadonlyフィールドには少し片手落ちなところがあって、実は、構造体の場合にちょっとした問題を起こせたりします。

構造体のメソッドの中ではthisが「自分自身の参照」の意味なんですが、このthis参照は書き換えできてしまいます。そのため、以下のように、readonlyで一見書き換えができなさそうなフィールドを書き換えてしまうことができます。

using System;structPoint{// フィールドに readonly を付けているものの…publicreadonlyint X;publicreadonlyint Y;publicPoint(int x,int y) => (X, Y) = (x, y);// this の書き換えができてしまうので、実は X, Y の書き換えが可能publicvoid Set(int x,int y)    {// X = x; Y = y; とは書けない// でも、this 自体は書き換えられるthis =newPoint(x, y);    }}classProgram{staticvoid Main()    {var p =newPoint(1, 2);// p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる// でも、このメソッドは呼べるし、X, Y が書き換わる        p.Set(3, 4);Console.WriteLine(p.X);// 3Console.WriteLine(p.Y);// 4    }}

わざわざこんな紛らわしいことをしようとは思わないのでめったに問題になることはないんですが、一応は注意が必要です。また、この問題は、次節で説明する通り、C# 7.2で少し緩和されます。

readonly struct

Ver. 7.2

C# 7.2で、構造体自体にreadonly修飾を付けられるようになりました。readonlyを付けた構造体は以下のような状態になります。

  • 全てのフィールドに対してreadonly を付けなければならなくなる
  • this参照もreadonly扱いされる

thisreadonly扱いになるので、前節のようなthis書き換えの問題は起きません。

using System;// 構造体自体に readonly を付けるreadonlystructPoint{// フィールドには readonly が必須publicreadonlyint X;publicreadonlyint Y;publicPoint(int x,int y) => (X, Y) = (x, y);// readonly を付けない場合と違って、以下のような this 書き換えも不可//public void Set(int x, int y) => this = newPoint(x, y);}classProgram{staticvoid Main()    {var p =newPoint(1, 2);// p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる// p.Set(3, 4); みたいなのもダメConsole.WriteLine(p.X);// 1 しかありえないConsole.WriteLine(p.Y);// 2 しかありえない    }}

readonly struct によるコピー回避

前述の通り、(無印の)構造体のreadonlyフィールドに対してメソッドを呼ぶと防衛的コピーが発生するという問題があります。これに対して、readonly structであれば、このコピーを回避できます。

例えば以下のように、ほぼ同じ構造・どちらも書き換え不能な構造体を作ったとして、readonly structになっているかどうかでコピー発生の有無が変わります。

using System;// 作りとしては readonly を意図しているので、何も書き換えしない// でも、struct 自体には readonly が付いていないstructNoReadOnly{publicreadonlyint X;publicvoid M() { }}//NoReadOnly と作りは同じ// ちゃんと readonly structreadonlystructReadOnly{publicreadonlyint X;publicvoid M() { }}classProgram{staticreadonlyNoReadOnly nro;staticreadonlyReadOnly ro;staticvoid Main()    {// readonly を付けなかった場合// フィールド参照(読み取り)は問題ないConsole.WriteLine(nro.X);// メソッド呼び出しが問題。ここでコピー発生// (呼び出し側では、「M の中で特に何も書き換えていない」というのを知るすべがないので、防衛的にコピーが発生)        nro.M();// readonly を付けた場合// これなら、M をそのまま呼んでも何も書き換わらない保証があるので、コピーは起きない        ro.M();    }// これも問題あり(コピー発生)// in を付けたので readonly 扱い → M を呼ぶ際にコピー発生staticvoid F(inNoReadOnly x) => x.M();// こちらも、readonly struct であれば問題なし(コピー回避)staticvoid F(inReadOnly x) => x.M();}

C# 7.2 以降では、書き換えを意図していない構造体に対してはreadonly修飾を付けるのが無難でしょう。

また、「フィールド直接参照なら大丈夫だけど、メソッドを(プロパティも)呼ぶとコピー発生」という性質上、書き換えを最初から意図している構造体の場合は、プロパティよりも、フィールドを直接publicにしてしまう方が都合がいいことがあります。

readonly参照と不変性

in引数ref readonlyで、読み取り専用の参照を作れます。この読み取り専用参照は、「そのメソッド内で書き換えない」、「その引数・変数を通した書き換えをしない」という意思表明としては非常に有用です。その一方で、「外で書き換わる」、「参照元の値が書き換わる」という意味で、不変性(immutability)の保証はありません。

例えば以下の例を見てください。

using System;classProgram{staticvoid Main()    {        _value = 0;        ByVal(_value);// 0, 0        _value = 0;        ByRef(_value);// 0, 1    }// 書き換えできるフィールドstaticint _value;// 値渡し = コピー なので、 _value 書き換えの影響は受けないstaticvoid ByVal(int value)    {Console.WriteLine(value);        _value++;Console.WriteLine(value);    }// 参照渡しなので、 _value 書き換えの影響を受ける// in (ref readonly) であっても、immutable ではない// value を通して書き換えない保証があるだけで、別経路で書き換わることに対しては無力staticvoid ByRef(inint value)    {Console.WriteLine(value);        _value++;Console.WriteLine(value);    }}

メソッドの中身としては全く同じメソッドが2つありますが、片方(ByVal)は値渡しで、もう片方(ByRef)はin 引数で整数値を受け取っています。ByValでは、valueは値のコピーを受け取っているので、元の値の出どころとは無縁になっています。一方、ByRefの方ではvalue自身はinが付いていて書き換えられませんが、その参照元になっている_value の方が書き換わると、valueの値も一緒に変化します。書き換え不能(readonly)だからと言って、値の不変性(immutable)の保証はなく、こうして値が変化する場合があります。

readonly 関数メンバー

Ver. 8.0

C# 8.0 で、関数メンバー単位で「フィールドを書き換えてない」ということを保証できるようになりました。構造体全体をreadonly struct にしなくても、隠れたコピー問題を避けられる機会が増えます。

以下のように、関数メンバーにreadonly 修飾を付けます。

// 構造体自体は readonly にしない。// フィールドは書き換えたいstructNonReadOnly{publicfloat X;publicfloat Y;// でも、このプロパティ内ではフィールドを書き換えないpublicfloat LengthSquared => X * X + Y * Y;}// NonReadOnly との差は LengthSquared の readonly の有無だけstructReadOnly{publicfloat X;publicfloat Y;// readonly 修飾でフィールドを書き換えないことを明示publicreadonlyfloat LengthSquared => X * X + Y * Y;}classProgram{// こっちは、LengthSquared 内での X, Y の書き換えを恐れて隠れたコピーが発生する。staticfloatM(inNonReadOnlyx) =>x.LengthSquared;// こっちは、LengthSquared に readonly が付いているのでコピー発生しない。staticfloatM(inReadOnlyx) =>x.LengthSquared;staticvoidMain(string[]args)    {M(newNonReadOnly { X = 1, Y = 2 });M(newReadOnly { X = 1, Y = 2 });    }}

隠れたコピー問題はソースコードの見た目に現れず、気づきにくい問題なので、関数内でフィールドを書き換えていないなら積極的にreadonly 修飾を付けておくべきでしょう。

ちなみに、逆に、readonly 関数メンバー内から、readonly ではないものを触ろうとしても隠れたコピーが発生します。例えば以下のコードでは、Aのフィールドを書き換えるIncrementメソッドを、readonly なメソッドとそうでないメソッドから呼び出してみています。

using System;structA{publicint Value;publicvoidIncrement() => Value++;}structB{publicA A;// A の非 readonly メンバーを呼ぶ。publicvoidMutable() => A.Increment();// Mutable との差は readonly 修飾が付いてるだけ。// this が書き換わらないように、A のコピーが作られる。A 自体には変化が起きない。publicreadonlyvoidImmutable() => A.Increment();}classProgram{staticvoidMain()    {varb =newB();Console.WriteLine(b.A.Value);// 初期状態: 0b.Mutable();Console.WriteLine(b.A.Value);// 意図通りの書き換え: 1b.Immutable();Console.WriteLine(b.A.Value);// 書き換わらない: 1 (Immutable の中で A のコピーが発生)    }}

注意: 似て非なるもの(ref readonly)

このreadonly 関数メンバーは、構文上、ref readonlyと似ているのでちょっと注意が必要かもしれません。

structS{publicint[] _value;// これは、読み取り専用参照を返すという意味。// _value 配列の中身が書き換わってもらっては困る。publicrefreadonlyint X =>ref _value[0];// これは、S 内のフィールド(この場合 _value) を書き換えないという意味。// _value 配列の中身が書き換わろうと知ったことではない。publicreadonlyrefint Y =>ref _value[0];// これは、上記2つの両方の意味。// _value 自体も書き換わらないし、_value の中身を書き換えてもらっても困るとき用。publicreadonlyrefreadonlyint Z =>ref _value[0];}

ちなみに、プロパティの場合はget/set それぞれ別にreadonly 指定ができます。当然ですが、ほとんどの場合は「get だけがreadonly」になると思われます。

structX{int _value;publicint Value    {readonlyget => _value;set => _value =value;    }}

更新履歴

更新:言語バージョンの指定

[C#]

ファイル ベース実行

[C#]

C# 14.0 の新機能

[C#]

更新:[雑記] オーバーロード解決

[C#]

型の分割定義 (partial)

[C#]

ブログ

C# 14 の破壊的変更点(First-class Span)

ファイナライザー

文字列リテラルを data セクションに UTF-8 で書き込む案

nameof(T<>)

First-class な Span 型


誤字等を見つけた場合や、ご意見・ご要望がございましたら、GitHub の Issues まで気兼ねなくご連絡ください。

[8]ページ先頭

©2009-2025 Movatter.jp