これは不変の構造体を宣言する適切な方法ですか?
public struct Pair
{
public readonly int x;
public readonly int y;
// Constructor and stuff
}
これがなぜ問題にぶつかるのか、私には考えられませんが、念のため確認したいと思います。
この例では、intを使用しました。代わりにクラスを使用したが、そのクラスも不変である場合はどうなりますか?それもうまくいくはずですよね?
public struct Pair
{
public readonly (immutableClass) x;
public readonly (immutableClass) y;
// Constructor and stuff
}
(余談ですが、Propertiesを使用するとより一般化され、変更が可能になることを理解していますが、この構造体は文字通り2つの値を格納することだけを目的としています。ここでは不変の質問にのみ興味があります。)
構造体を使用する場合は、構造体を不変にすることをお勧めします。
すべてのフィールドを読み取り専用にすることは、(1)構造体が不変であることを文書化し、(2)偶発的な変更を防ぐための優れた方法です。
ただし、しわが1つありますが、実際には奇妙なことに偶然、来週ブログを書く予定でした。つまり、構造体フィールドの読み取り専用は嘘です。読み取り専用フィールドは変更できないことを期待していますが、もちろん変更できます。構造体フィールドの「読み取り専用」は、口座にお金がない小切手を書く宣言です。 構造体はそのストレージを所有しておらず、変更できるのはそのストレージです。
たとえば、あなたの構造体を見てみましょう:
public struct Pair
{
public readonly int x;
public readonly int y;
public Pair(int x, int y)
{
this.x = x;
this.y = y;
}
public void M(ref Pair p)
{
int oldX = x;
int oldY = y;
// Something happens here
Debug.Assert(x == oldX);
Debug.Assert(y == oldY);
}
}
デバッグアサーションに違反する原因となる「何かがここで発生する」ときに発生する可能性があるものはありますか?承知しました。
public void M(ref Pair p)
{
int oldX = this.x;
int oldY = this.y;
p = new Pair(0, 0);
Debug.Assert(this.x == oldX);
Debug.Assert(this.y == oldY);
}
...
Pair myPair = new Pair(10, 20);
myPair.M(ref myPair);
そして今、何が起こりますか?アサーションに違反しています! 「this」と「p」は同じ保管場所を指します。保管場所が変更され、「this」の内容も同じなので変更されます。構造体はストレージを所有していないため、構造体はxとyの読み取り専用を強制できません。ストレージはローカル変数であり、自由に変更できます。
構造体の読み取り専用フィールドが変化することが決して観察されないという不変式ではrelyできません。信頼できる唯一のことは、コードを直接変更するコードを記述できないことです。しかし、このような少しこっそりした作業で、間接的に好きなように変更できます。
この問題に関するJoe Duffyの優れたブログ記事も参照してください。
http://joeduffyblog.com/2010/07/01/when-is-a-readonly-field-not-readonly/
それは確かにそれを不変にします。ただし、コンストラクタを追加した方がよいと思います。
そのすべてのメンバーも不変である場合、完全に不変になります。これらは、クラスまたは単純な値です。
C#7.2では、構造体全体を不変として宣言できるようになりました。
public readonly struct Pair
{
public int x;
public int y;
// Constructor and stuff
}
これは、すべてのフィールドをreadonly
としてマークするのと同じ効果があり、構造体が不変であることをコンパイラー自体に文書化します。これにより、コンパイラーが作成する防御コピーの数を減らすことにより、構造体が使用される領域のパフォーマンスが向上します。
Eric Lippertの回答 で述べたように、これは構造自体が完全に再割り当てされることを妨げず、したがって、フィールドがあなたの下から変化する効果を提供します。これを防ぐには、値渡しまたは新しいin
パラメータ修飾子を使用できます。
public void DoSomething(in Pair p) {
p.x = 0; // illegal
p = new Pair(0, 0); // also illegal
}
コンパイラは、readonly
フィールドおよび読み取り専用プロパティへの割り当てを禁止します。
私は、主にパブリックインターフェイスの理由とデータバインディング(フィールドでは機能しない)のために、読み取り専用プロパティを使用することをお勧めします。それが私のプロジェクトである場合、構造体/クラスがパブリックである場合、それが必要になります。アセンブリの内部またはクラスのプライベートになる場合、最初は見落とし、後でそれらを読み取り専用プロパティにリファクタリングできます。