SOに関するここでの議論に続いて、可変構造体は「悪」であるという発言をすでに数回読みました(この質問への回答のように)。
C#の可変性と構造体の実際の問題は何ですか?
SOに関するここでの議論に続いて、可変構造体は「悪」であるという発言をすでに数回読みました(この質問への回答のように)。
C#の可変性と構造体の実際の問題は何ですか?
構造体は値型です。つまり、渡されるときにコピーされます。
したがって、コピーを変更すると、そのコピーのみが変更され、元のコピーは変更されず、他のコピーは変更されません。
構造体が不変の場合、値渡しによるすべての自動コピーは同じになります。
変更したい場合は、変更されたデータで構造体の新しいインスタンスを作成して、意識的に行う必要があります。(コピーではありません)
どこから始めるか;-p
Eric Lippertのブログは、常に引用に適しています。
これは、可変値型が悪であるもう1つの理由です。常に値型を不変にするようにしてください。
まず、変更を簡単に失う傾向があります...たとえば、リストから物事を取り出す場合:
Foo foo = list[0];
foo.Name = "abc";
それは何を変えましたか?何も役に立ちません...
プロパティについても同じです。
myObj.SomeProperty.Size = 22; // the compiler spots this one
あなたに強制する:
Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;
それほど重要ではありませんが、サイズの問題があります。可変オブジェクトは複数のプロパティを持つ傾向があります。ただし、2つint
のs、a string
、a、DateTime
およびaを持つ構造体がある場合はbool
、大量のメモリを非常にすばやく焼き尽くすことができます。クラスを使用すると、複数の呼び出し元が同じインスタンスへの参照を共有できます(参照は小さいです)。
私は悪とは言いませんが、可変性は多くの場合、最大限の機能を提供しようとするプログラマー側の熱心さの表れです。実際には、これは必要ないことが多く、その結果、インターフェイスが小さくなり、使いやすくなり、間違った使い方が難しくなります (= より堅牢になります)。
この一例は、競合状態での読み取り/書き込みおよび書き込み/書き込みの競合です。書き込みは有効な操作ではないため、これらは不変の構造では発生しません。
また、可変性が実際に必要になることはほとんどないと私は主張します。たとえば、日付を変更しても意味がありません。むしろ、古い日付に基づいて新しい日付を作成してください。これは安価な操作であるため、パフォーマンスは考慮されません。
public の変更可能なフィールドまたはプロパティを持つ構造体は悪ではありません。
「this」を変更する構造体メソッド (プロパティ セッターとは異なります) は、.net がそれらを変更しないメソッドと区別する手段を提供しないという理由だけで、いくぶん邪悪です。「this」を変更しない構造体メソッドは、防御的なコピーを必要とせずに、読み取り専用の構造体でも呼び出すことができる必要があります。「this」を変更するメソッドは、読み取り専用の構造体ではまったく呼び出し可能であってはなりません。.net は、「this」を変更しない構造体メソッドが読み取り専用構造体で呼び出されることを禁止したくないが、読み取り専用構造体が変更されることを許可したくないため、読み取り専用構造体を防御的にコピーします。コンテキストのみであり、間違いなく両方の世界で最悪の事態になります。
ただし、読み取り専用コンテキストでの自己変更メソッドの処理に関する問題にもかかわらず、可変構造体は多くの場合、可変クラス型よりもはるかに優れたセマンティクスを提供します。次の 3 つのメソッド シグネチャを検討してください。
struct PointyStruct {public int x,y,z;}; クラス PointyClass {public int x,y,z;}; void Method1(PointyStruct foo); void Method2(ref PointyStruct foo); void Method3(PointyClass foo);
それぞれの方法について、次の質問に答えてください。
答え:
質問 1:
Method1()
: いいえ(明確な意図)
Method2()
: はい(明確な意図)
Method3()
: はい(不確実な意図)
質問 2:
Method1()
: いいえ
Method2()
: いいえ(安全でない場合を除く)
Method3()
: はい
Method1 は foo を変更できず、参照も取得できません。Method2 は foo への短命の参照を取得します。これを使用して、戻るまで、foo のフィールドを任意の回数、任意の順序で変更できますが、その参照を永続化することはできません。Method2 が復帰する前に、安全でないコードを使用しない限り、「foo」参照から作成された可能性のあるすべてのコピーが消失します。Method2 とは異なり、Method3 は無差別に共有可能な foo への参照を取得します。foo をまったく変更しないかもしれませんし、foo を変更してから戻るかもしれませんし、別のスレッドに foo への参照を与えるかもしれません。
構造体の配列は素晴らしいセマンティクスを提供します。Rectangle 型の RectArray[500] が与えられた場合、要素 123 を要素 456 にコピーし、しばらくして要素 123 の幅を要素 456 に影響を与えずに 555 に設定する方法は明らかです。 ]; ...; RectArray[123].Width = 555;". Rectangle が Width と呼ばれる整数フィールドを持つ構造体であることを知っていれば、上記のステートメントについて知っておく必要があるすべてのことがわかります。
ここで、RectClass が Rectangle と同じフィールドを持つクラスであり、RectClass 型の RectClassArray[500] に対して同じ操作を実行したいとします。おそらく、配列は、変更可能な RectClass オブジェクトへの、事前に初期化された不変の参照を 500 個保持することになっています。その場合、適切なコードは "RectClassArray[321].SetBounds(RectClassArray[456]); ...; RectClassArray[321].X = 555;" のようになります。おそらく、配列は変更されないインスタンスを保持すると想定されるため、適切なコードは "RectClassArray[321] = RectClassArray[456]; ...; RectClassArray[321] = New RectClass(RectClassArray[321] のようになります。 ]); RectClassArray[321].X = 555;" 何をすべきかを知るには、RectClass についてもっと多くのことを知る必要があります (たとえば、コピー コンストラクター、コピー元メソッドなどをサポートしているかどうかなど)。) および配列の使用目的。構造体を使用するほどきれいにはなりません。
確かに、残念ながら、配列以外のコンテナー クラスが構造体配列のクリーンなセマンティクスを提供する良い方法はありません。コレクションに文字列などでインデックスを付けたい場合、最善の方法はおそらく、インデックスの文字列、ジェネリックパラメーター、および渡されるデリゲートを受け入れるジェネリック「ActOnItem」メソッドを提供することです。ジェネリック パラメーターとコレクション アイテムの両方を参照します。これにより、構造体配列とほぼ同じセマンティクスが可能になりますが、vb.net と C# の人々が適切な構文を提供するように説得されない限り、コードは、たとえパフォーマンスが合理的であっても (ジェネリック パラメーターを渡すと静的デリゲートの使用を許可し、一時的なクラス インスタンスを作成する必要がなくなります)。
個人的には、エリック・リッパートらへの憎しみにうんざりしています。可変値型に関する情報を吐き出します。それらは、いたるところで使用されている乱雑な参照型よりもはるかにクリーンなセマンティクスを提供します。値型に対する .net のサポートにはいくつかの制限がありますが、変更可能な値型が他の種類のエンティティよりも適している場合が多くあります。
プログラマーの観点からは、予期しない動作につながる可能性のあるコーナー ケースが他にもいくつかあります。
// Simple mutable structure.
// Method IncrementI mutates current state.
struct Mutable
{
public Mutable(int i) : this()
{
I = i;
}
public void IncrementI() { I++; }
public int I { get; private set; }
}
// Simple class that contains Mutable structure
// as readonly field
class SomeClass
{
public readonly Mutable mutable = new Mutable(5);
}
// Simple class that contains Mutable structure
// as ordinary (non-readonly) field
class AnotherClass
{
public Mutable mutable = new Mutable(5);
}
class Program
{
void Main()
{
// Case 1. Mutable readonly field
var someClass = new SomeClass();
someClass.mutable.IncrementI();
// still 5, not 6, because SomeClass.mutable field is readonly
// and compiler creates temporary copy every time when you trying to
// access this field
Console.WriteLine(someClass.mutable.I);
// Case 2. Mutable ordinary field
var anotherClass = new AnotherClass();
anotherClass.mutable.IncrementI();
// Prints 6, because AnotherClass.mutable field is not readonly
Console.WriteLine(anotherClass.mutable.I);
}
}
構造体の配列があり、その配列の最初の要素のメソッドをMutable
呼び出しているとします。IncrementI
この呼び出しからどのような動作を期待していますか? 配列の値を変更するか、コピーのみを変更する必要がありますか?
Mutable[] arrayOfMutables = new Mutable[1];
arrayOfMutables[0] = new Mutable(5);
// Now we actually accessing reference to the first element
// without making any additional copy
arrayOfMutables[0].IncrementI();
// Prints 6!!
Console.WriteLine(arrayOfMutables[0].I);
// Every array implements IList<T> interface
IList<Mutable> listOfMutables = arrayOfMutables;
// But accessing values through this interface lead
// to different behavior: IList indexer returns a copy
// instead of an managed reference
listOfMutables[0].IncrementI(); // Should change I to 7
// Nope! we still have 6, because previous line of code
// mutate a copy instead of a list value
Console.WriteLine(listOfMutables[0].I);
したがって、変更可能な構造体は、あなたとチームの他のメンバーが何をしているのかを明確に理解している限り、悪ではありません。しかし、プログラムの動作が予想とは異なる場合があまりにも多く、微妙な生成や理解が困難なエラーにつながる可能性があります。
値型は基本的に不変の概念を表します。Fx、整数、ベクトルなどの数学的な値を持っていて、それを変更できるのは意味がありません。それは、値の意味を再定義するようなものです。値の型を変更する代わりに、別の一意の値を割り当てる方が理にかなっています。プロパティのすべての値を比較することによって、値の型が比較されるという事実について考えてみてください。ポイントは、プロパティが同じであれば、その値の同じ普遍的な表現であるということです。
Konrad が言及しているように、日付を変更することも意味がありません。値はその一意の時点を表し、状態やコンテキスト依存性を持つ時間オブジェクトのインスタンスではないためです。
これがあなたにとって意味があることを願っています。確かに、実際的な詳細よりも、値の型でキャプチャしようとする概念に関するものです。
C/C++ などの言語でプログラミングしたことがある場合は、構造体を可変として使用しても問題ありません。それらをrefで渡すだけで、問題が発生することはありません。私が見つけた唯一の問題は、C#コンパイラの制限であり、場合によっては、構造体がC#クラスの一部である場合のように、コピーの代わりに構造体への参照を使用するように愚かなことを強制することはできません。 )。
したがって、変更可能な構造体は悪ではなく、C# によって悪になりました。私は常に C++ で変更可能な構造体を使用していますが、それらは非常に便利で直感的です。対照的に、C# では、オブジェクトを処理する方法が原因で、構造体をクラスのメンバーとして完全に放棄するようになりました。彼らの利便性は、私たちの利便性を犠牲にしました。
1,000,000 個の構造体の配列があるとします。bid_price、offer_price (おそらく小数) などの株式を表す各構造体は、C#/VB によって作成されます。
アンマネージ ヒープに割り当てられたメモリ ブロックに配列が作成され、他のネイティブ コード スレッドが配列に同時にアクセスできるようになるとします (おそらく、計算を行う高パフォーマンス コード)。
C#/VB コードが価格変更のマーケット フィードをリッスンしていると想像してください。そのコードは、配列の一部の要素にアクセスし (セキュリティに関係なく)、一部の価格フィールドを変更する必要がある場合があります。
これが毎秒何万回、あるいは何十万回も行われていると想像してみてください。
事実に直面しましょう。この場合、これらの構造体を変更可能にする必要があります。それらは他のネイティブ コードによって共有されているため、コピーを作成しても役に立たないためです。これらのレートで約 120 バイトの構造体のコピーを作成するのは、特に更新が実際に 1 つか 2 バイトだけに影響を与える可能性がある場合、狂気であるため、そうする必要があります。
ヒューゴ
何かを変異させることができると、アイデンティティの感覚が得られます。
struct Person {
public string name; // mutable
public Point position = new Point(0, 0); // mutable
public Person(string name, Point position) { ... }
}
Person eric = new Person("Eric Lippert", new Point(4, 2));
Person
はミュータブルであるため、 Eric のクローンを作成してクローンを移動し、元の. どちらの操作でも の内容を変更eric.position
できますが、一方は他方よりも直感的です。同様に、Eric を変更するメソッドについて (参照として) Eric を渡す方がより直感的です。メソッドに Eric のクローンを与えることは、ほとんどの場合驚くべきことです。突然変異を起こしたい人Person
は、への参照を求めることを忘れないでくださいPerson
。そうしないと、間違ったことをすることになります。
型を不変にすると、問題はなくなります。を変更できない場合はeric
、 を受け取るeric
か複製するかに関係ありませんeric
。より一般的には、次のいずれかのメンバーですべての監視可能な状態が保持されている場合、型は値渡ししても安全です。
これらの条件が満たされている場合、可変値型は参照型のように動作します。これは、浅いコピーでも受信者が元のデータを変更できるためです。
不変の直感性は、Person
何をしようとしているのかによって異なります。人に関する一連のデータをPerson
表すだけであれば、それについて直感的でないことは何もありません。変数は、オブジェクトではなく、真に抽象的な値を表します。(その場合は、名前を に変更する方がおそらく適切でしょう。)が実際に人物そのものをモデリングしている場合、変更していると考える落とし穴を回避できたとしても、クローンを常に作成して移動するという考えはばかげています。オリジナル。その場合は、単純に参照型 (つまり、クラス) を作成する方が自然でしょう。Person
PersonData
Person
Person
確かに、関数型プログラミングが教えてくれたように、すべてを不変にすることには利点があります (誰も密かに参照を保持して変更することはできeric
ません)。コード。
可変データには多くの長所と短所があります。百万ドルの欠点はエイリアシングです。同じ値が複数の場所で使用されていて、そのうちの 1 つが変更された場合、それを使用している他の場所では魔法のように変更されたように見えます。これは競合状態に関連していますが、同一ではありません。
100 万ドルの利点は、場合によってはモジュール性です。可変状態を使用すると、変更情報を知る必要のないコードから変更情報を隠すことができます。
Art of the Interpreterでは、これらのトレードオフについて詳しく説明し、いくつかの例を示します。
個人的にコードを見ると、次のコードはかなり不格好に見えます。
data.value.set (data.value.get () + 1);
単にではなく
データ.値++; または data.value = data.value + 1 ;
データのカプセル化は、クラスを渡し、制御された方法で値を変更したい場合に便利です。しかし、渡されたものに値を設定するだけの public set および get 関数がある場合、これは単純に public データ構造を渡すよりもどのように改善されるでしょうか?
クラス内にプライベート構造を作成するときは、一連の変数を 1 つのグループに編成するためにその構造を作成しました。その構造のコピーを取得して新しいインスタンスを作成するのではなく、クラス スコープ内でその構造を変更できるようにしたいと考えています。
私にとって、これは、パブリック変数を整理するために使用される構造の有効な使用を妨げます。アクセス制御が必要な場合は、クラスを使用します。
構造体とは何の関係もありません (C# とも関係ありません) が、Java では可変オブジェクトがハッシュ マップのキーなどである場合に問題が発生する可能性があります。それらをマップに追加した後にそれらを変更すると、そのハッシュ コードが変更され、悪いことが起こる可能性があります。
Eric Lippert 氏の例にはいくつかの問題があります。これは、構造体がコピーされるという点と、注意しないとそれがどのように問題になる可能性があるかを説明するために考案されたものです。例を見ると、プログラミングの悪い習慣の結果であり、実際には構造体またはクラスの問題ではありません。
構造体は、パブリック メンバーのみを持ち、カプセル化を必要としません。もしそうなら、それは本当にタイプ/クラスでなければなりません。同じことを言うのに 2 つの構文は必要ありません。
構造体を囲むクラスがある場合は、クラス内のメソッドを呼び出してメンバー構造体を変更します。これは、良いプログラミングの習慣として私が行うことです。
適切な実装は次のようになります。
struct Mutable {
public int x;
}
class Test {
private Mutable m = new Mutable();
public int mutate()
{
m.x = m.x + 1;
return m.x;
}
}
static void Main(string[] args) {
Test t = new Test();
System.Console.WriteLine(t.mutate());
System.Console.WriteLine(t.mutate());
System.Console.WriteLine(t.mutate());
}
構造体自体の問題ではなく、プログラミングの習慣の問題のようです。構造体は変更可能であると想定されています。それがアイデアと意図です。
変更の結果、出来上がりは期待どおりに動作します。
1 2 3 いずれかのキーを押して続行します。. .