可変構造体が「悪」なのはなぜですか?
-
22-07-2019 - |
質問
SOでの議論に続いて、可変構造体は“ evil”であるという発言を何度か読みました。 (この質問に対する回答のようにa>)。
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
、 string
、 DateTime
、および bool
を持つ構造体がある場合は、非常にすばやく多くのメモリを使い果たします。クラスを使用すると、複数の呼び出し元が同じインスタンスへの参照を共有できます(参照は小さいです)。
邪悪とは言いませんが、可変性は多くの場合、プログラマーが最大限の機能を提供しようとする過剰な熱心さの表れです。実際には、これは多くの場合必要ではないため、インターフェイスが小さくなり、使いやすくなり、間違った使用が難しくなります(=より堅牢)。
この一例は、競合状態での読み取り/書き込みおよび書き込み/書き込みの競合です。書き込みは有効な操作ではないため、これらは不変構造では発生しません。
また、可変性は実際にはほとんどないと主張します必要 、プログラマーは将来 になる可能性があると考える。たとえば、日付を変更しても意味がありません。むしろ、古い日付に基づいて新しい日付を作成します。これは安価な操作なので、パフォーマンスは考慮されません。
可変構造体は悪ではありません。
これらは、高性能の環境では絶対に必要です。たとえば、キャッシュラインやガベージコレクションがボトルネックになる場合。
これらの完全に有効なユースケース「悪」で不変の構造体の使用を呼び出すことはありません。
C#の構文は値型または参照型のメンバーのアクセスを区別するのに役立たないという点を見ることができます。 、可変構造体上。
ただし、単に不変の構造体に「悪」というラベルを付けるのではなく、言語を採用し、より有用で建設的な経験則を支持することをお勧めします。
例:"構造体は値タイプであり、デフォルトでコピーされます。コピーしたくない場合は参照が必要です。" または "最初に読み取り専用の構造体を使用してみてください。
パブリックな可変フィールドまたはプロパティを持つ構造は悪ではありません。
「this」を変異させる構造メソッド(プロパティセッターとは異なる) .netが提供していないメソッドと区別する手段を提供していないためです。 「this」を変更しない構造体メソッド読み取り専用の構造体でも、防御的なコピーを必要とせずに呼び出すことができます。 「this」を変更するメソッド読み取り専用の構造体ではまったく呼び出せません。 .netは" this"を変更しないstructメソッドを禁止したくないため、読み取り専用の構造体で呼び出されますが、読み取り専用の構造体を変更したくない場合、読み取り専用のコンテキストで構造体を防御的にコピーし、おそらく両方の世界の最悪の事態になります。
ただし、読み取り専用コンテキストでの自己変更メソッドの処理には問題がありますが、可変構造体は多くの場合、可変クラス型よりもはるかに優れたセマンティクスを提供します。次の3つのメソッドシグネチャを考慮してください。
struct PointyStruct {public int x,y,z;}; class PointyClass {public int x,y,z;}; void Method1(PointyStruct foo); void Method2(ref PointyStruct foo); void Method3(PointyClass foo);
各方法について、次の質問に答えます:
- メソッドが「安全でない」ものを使用しないと仮定します。コード、fooを変更できますか?
- メソッドが呼び出される前に「foo」への外部参照が存在しない場合、後に外部参照が存在する可能性はありますか?
回答:
質問1:
 Method1()
:いいえ(明確な意図)
 Method2()
:はい(明確な意図)
 Method3()
:はい(不確実な意図)
質問2:
 Method1()
:いいえ
 Method2()
:いいえ(安全でない場合)
 Method3()
:はい
Method1はfooを変更できず、参照を取得しません。 Method2は、fooへの短命の参照を取得します。この参照を使用して、fooのフィールドを何度でも任意の順序で、返されるまで変更できますが、その参照を保持することはできません。 Method2が戻る前に、安全でないコードを使用しない限り、 'foo'参照から作成された可能性のあるすべてのコピーが消えます。 Method3は、Method2とは異なり、fooへの無差別に共有可能な参照を取得します。これを使用して何を行うかはわかりません。 fooをまったく変更しないか、fooを変更してから戻るか、fooの参照を別のスレッドに与え、将来の任意の時点で任意の方法で変更する可能性があります。 Method3が渡される可変クラスオブジェクトに対して行うことを制限する唯一の方法は、可変オブジェクトを読み取り専用ラッパーにカプセル化することです。これは見苦しくて面倒です。
構造の配列は素晴らしいセマンティクスを提供します。タイプRectangleのRectArray [500]が与えられると、どのようにすれば明確かつ明白になります。要素123を要素456にコピーし、しばらくしてから要素456を乱すことなく要素123の幅を555に設定します。" RectArray [432] = RectArray [321]; ...; 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] =新しいRectClass(RectClassArray [321]); RectClassArray [321] .X = 555;"自分が何をすべきかを知るには、RectClass(e。
値型は基本的に不変の概念を表します。 Fx、整数、ベクトルなどの数学的な値を持ち、それを変更できるようにすることは意味がありません。それは、値の意味を再定義するようなものです。値の種類を変更する代わりに、別の一意の値を割り当てる方が合理的です。プロパティのすべての値を比較することで、値型が比較されるという事実を考えてください。ポイントは、プロパティが同じであれば、その値の同じ普遍的な表現であるということです。
Konradが言及しているように、値はその一意の時点を表し、状態またはコンテキスト依存性を持つtimeオブジェクトのインスタンスではないため、日付を変更することも意味がありません。
これはあなたにとって意味のあることです。確かに、実際の詳細よりも、値型でキャプチャしようとする概念についてです。
プログラマの観点から予測できない動作を引き起こす可能性のある別のいくつかのコーナーケースがあります。ここにそれらのカップル。
- 不変の値型と読み取り専用フィールド
// 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);
したがって、可変構造は、あなたとチームの他のメンバーがあなたが何をしているかを明確に理解している限り、悪ではありません。しかし、プログラムの振る舞いが予想される振る舞いと異なる場合は、あまりにも多くのコーナーケースがあり、そのため微妙なエラーの生成と理解が困難になります。
C / C ++などの言語でプログラミングしたことがある場合、構造体は可変として使用しても問題ありません。 refでそれらを渡すだけで、何も間違ったことはありません。私が見つける唯一の問題は、C#コンパイラの制限であり、場合によっては、愚かなことに強制的にコピーの代わりに構造体への参照を使用することができません(構造体がC#クラスの一部である場合のように) )。
したがって、可変構造体は悪ではなく、C#はそれらを悪 しています。私は常にC ++で可変構造体を使用しており、非常に便利で直感的です。対照的に、C#を使用すると、オブジェクトの処理方法のために、構造体をクラスのメンバーとして完全に放棄することになりました。彼らの利便性は私たちにコストをかけました。
1,000,000の構造体の配列があるとします。 bid_price、offer_price(おそらく小数)などのようなものを持つエクイティを表す各構造体は、C#/ VBによって作成されます。
アンマネージヒープに割り当てられたメモリブロックに配列が作成され、他のネイティブコードスレッドが配列に同時にアクセスできるようになることを想像してください(おそらく、高パフォーマンスのコードが計算を実行します)。
C#/ VBコードが価格変更のマーケットフィードをリッスンしていることを想像してください。そのコードは配列の要素(いずれかのセキュリティ)にアクセスし、いくつかの価格フィールドを変更する必要があります。
これが1秒あたり数万回から数十万回行われていると想像してください。
まあ事実に直面することができます。この場合、これらの構造体を変更可能にしたいのです。他のネイティブコードと共有されているため、コピーを作成しても助けにはなりません。これらのレートで120バイトの構造体のコピーを作成するのは、特に更新が実際に1バイトまたは2バイトだけに影響を与える可能性がある場合は、気が狂いやすいためです。
ヒューゴ
構造体の目的(C#、Visual Basic 6、Pascal / Delphi、C ++構造体タイプ(またはクラス)がポインターとして使用されていない場合)に固執すると、構造体が複合変数。つまり、共通名(メンバーを参照するレコード変数)の下で、変数のパックセットとしてそれらを扱います。
OOPに深く慣れている多くの人々を混乱させることはわかっていますが、それが正しく使用されていれば、それが本質的に悪であると言う十分な理由はありません。一部の構造は意図したとおりに不変です(これはPythonの namedtuple
の場合です)が、考慮すべき別のパラダイムです。
はい:構造体は多くのメモリを必要としますが、次のようにすると正確にメモリが増えるわけではありません:
point.x = point.x + 1
比較対象:
point = Point(point.x + 1, point.y)
メモリ消費量は、少なくとも同じか、不変の場合はさらに大きくなります(ただし、その場合は、言語に応じて現在のスタックでは一時的なものになります)。
しかし最後に、構造はオブジェクトではなく構造です。 POOでは、オブジェクトの主なプロパティは identity であり、ほとんどの場合、それはメモリアドレス以下です。 Structはデータ構造(適切なオブジェクトではないため、IDを持たない)を表し、データを変更できます。他の言語では、(Pascalの場合のように struct の代わりに) record が単語であり、同じ目的を持ちます:データレコード変数のみを読み取りますファイルから、変更され、ファイルにダンプされます(主な用途であり、多くの言語では、レコード内でデータの配置を定義することもできますが、適切に呼び出されたオブジェクトの場合は必ずしもそうではありません)。
良い例が必要ですか?構造体は、ファイルを簡単に読み取るために使用されます。 Pythonにはこのライブラリがあります。これは、オブジェクト指向であり、構造体をサポートしていないため、別の方法で実装しますが、これはややいです。構造体を実装する言語には、その機能が組み込まれています。 PascalやCなどの言語で適切な構造体を使用してビットマップヘッダーを読み取ってみてください(構造体が適切に構築および配置されている場合、Pascalではレコードベースのアクセスを使用せず、任意のバイナリデータを読み取る機能を使用します)。そのため、ファイルおよび直接(ローカル)メモリアクセスの場合、構造体はオブジェクトよりも優れています。今日に関しては、JSONとXMLに慣れているため、バイナリファイルの使用を忘れています(副作用として、構造体の使用も)。しかし、はい:それらは存在し、目的を持っています。
彼らは悪ではありません。それらを正しい目的に使用するだけです。
ハンマーの観点から考えると、ネジを釘として扱い、ネジを壁に突っ込むのが難しく、ネジのせいで、邪悪なものになります。
何かが変異されると、アイデンティティの感覚が得られます。
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.position
の内容の変更に成功しますが、一方は他方よりも直感的です。同様に、Ericを変更するメソッドの(リファレンスとして)Ericを渡す方がより直感的です。メソッドにEricのクローンを与えることは、ほとんどの場合驚くべきことです。 Person
を変更したい人は、必ず Person
への参照を要求する必要があります。そうしないと、間違ったことをします。
型を不変にすると、問題はなくなります。 eric
を変更できない場合でも、 eric
を受け取っても、 eric
のクローンを受け取っても違いはありません。より一般的には、観察可能な状態のすべてが次のいずれかのメンバーで保持されている場合、型は値で渡しても安全です。
- 不変
- 参照タイプ
- 値渡ししても安全
これらの条件が満たされている場合、浅いコピーでもレシーバが元のデータを変更できるため、可変値タイプは参照タイプのように動作します。
不変の Person
の直感性は、何をしようとしているかによって異なります。 Person
が個人に関するデータセットを単に表している場合、それについて直感的ではないものはありません。 Person
変数は、オブジェクトではなく、抽象的な値を真に表します。 (その場合、おそらく名前を PersonData
に変更する方が適切でしょう。) Person
が実際に人自身をモデリングしている場合、クローンを絶えず作成および移動するという考えあなたがオリジナルを修正していると思うことの落とし穴を避けたとしても、ばかげています。その場合は、単に Person
を参照型(つまり、クラス)にする方が自然でしょう。
機能的プログラミングが教えてくれたように、すべてを不変にすることには利点があります( eric
への参照を密かに保持して彼を変異させることはできません)それはOOPで慣用的ではないので、あなたのコードで作業している他の誰にとっても直観的ではありません。
構造体とは何の関係もありません(C#とも関係ありません)が、Javaでは、可変オブジェクトに問題がある場合があります。ハッシュマップのキー。マップに追加した後にそれらを変更し、そのハッシュコード、悪が起こる可能性があります。
個人的にコードを見ると、次のことが非常に不格好に見えます:
data.value.set(data.value.get()+ 1);
単純ではなく
data.value ++;またはdata.value = data.value + 1;
データのカプセル化は、クラスを渡し、制御された方法で値が変更されるようにする場合に役立ちます。ただし、パブリックセットがあり、値をこれまでに渡された値に設定する以上の機能を持たない関数を取得する場合、単純にパブリックデータ構造を渡すことよりもどのように改善されますか?
クラス内にプライベート構造を作成するとき、変数のセットを1つのグループに整理するためにその構造を作成しました。クラススコープ内でその構造を変更できるようにしたいのですが、その構造のコピーを取得して新しいインスタンスを作成するのではありません。
これにより、パブリック変数の整理に使用される構造の有効な使用が妨げられます。アクセス制御が必要な場合は、クラスを使用します。
エリック・リッパート氏の例にはいくつかの問題があります。構造体がコピーされるポイントと、注意しないとどのように問題になるかを説明するために工夫されています。この例を見ると、プログラミングの悪さの結果であり、実際には構造体やクラスの問題ではありません。
-
構造体はパブリックメンバーのみを持つことになっているため、カプセル化は必要ありません。もしそうなら、それは本当にタイプ/クラスでなければなりません。同じことを言うのに、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 何かキーを押すと続行します 。 。 。
可変データには多くの利点と欠点があります。 100万ドルの欠点はエイリアシングです。同じ値が複数の場所で使用されており、そのうちの1つがそれを変更した場合、それを使用している他の場所に魔法のように変更されたように見えます。これは、競合状態に関連していますが、同一ではありません。
100万ドルの利点は、モジュール性です。可変状態を使用すると、それについて知る必要のないコードから変更情報を隠すことができます。
通訳者の芸術はこれらのトレードオフをある程度詳しく説明し、いくつかの例。
正しく使用した場合、それらが悪だとは思わない。実動コードには入れませんが、構造体の単体テストモックなど、構造体の寿命が比較的短いものには使用します。
Ericの例を使用して、おそらくそのEricの2番目のインスタンスを作成しますが、テストの性質(つまり、複製、変更)に応じて調整を行います。テストの比較としてEricを使用する予定がない限り、テストスクリプトの残りの部分でEric2を使用している場合は、Ericの最初のインスタンスで何が起こるかは関係ありません。
これは、特定のオブジェクト(構造体のポイント)を浅く定義するレガシーコードをテストまたは変更するのに役立ちますが、不変の構造体を使用することで、迷惑な使用を防ぎます。