web-dev-qa-db-ja.com

可変構造体が「悪」なのはなぜですか?

ここでのSOの議論に続いて、可変構造体は「悪」であるという発言を何度も読みました(これに対する答えのように question )。

C#の可変性と構造体の実際の問題は何ですか?

464
Dirk Vollmar

構造体は値型であり、渡されるときにコピーされます。

そのため、コピーを変更すると、そのコピーのみが変更され、元のコピーは変更されず、他のコピーは変更されません。

構造体が不変の場合、値で渡された結果の自動コピーはすべて同じになります。

変更したい場合は、変更されたデータを使用して構造体の新しいインスタンスを作成することにより、意識的に変更する必要があります。 (コピーではない)

279
trampster

開始場所;-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;

それほど重要ではないが、サイズの問題があります。可変オブジェクトtendは複数のプロパティを持ちます。しかし、2つのints、stringDateTimeboolの2つの構造体がある場合、大量のメモリを非常にすばやく燃やすことができます。クラスを使用すると、複数の呼び出し元が同じインスタンスへの参照を共有できます(参照はわずかです)。

164
Marc Gravell

evilとは言いませんが、可変性は多くの場合、プログラマーが最大限の機能を提供するための過剰な熱心さの表れです。実際には、これは多くの場合必要ではないため、インターフェイスが小さくなり、使いやすくなり、間違った使用が難しくなります(=より堅牢)。

この1つの例は、競合状態での読み取り/書き込みおよび書き込み/書き込みの競合です。書き込みは有効な操作ではないため、これらは不変構造では発生しません。

また、可変性は実際にはほとんど必要ない 、プログラマはただと思うmight将来になる可能性があること。たとえば、日付を変更しても意味がありません。むしろ、古い日付に基づいて新しい日付を作成します。これは安価な操作であるため、パフォーマンスは考慮されません。

68
Konrad Rudolph

可変構造体は悪ではありません。

これらは、高性能の環境では絶対に必要です。たとえば、キャッシュラインやガベージコレクションがボトルネックになる場合。

これらの完全に有効なユースケースでの不変の構造体の使用を「悪」とは呼びません。

C#の構文は値型または参照型のメンバーのアクセスを区別するのに役立たないという点を見ることができるので、私はすべてpreferring不変の構造体。不変の構造体よりも不変性を強制します。

ただし、単に不変の構造体を「悪」とラベル付けするのではなく、言語を採用し、より有用で建設的な経験則を支持することをお勧めします。

例: "構造体はデフォルトでコピーされる値タイプです。コピーしたくない場合は参照が必要です"または "最初に読み取り専用の構造体を使用してみてください"

41
JE42

パブリックな可変フィールドまたはプロパティを持つ構造は悪ではありません。

"this"を変更する(プロパティセッターとは異なる)構造体メソッドは、.netがそうでないメソッドとそれらを区別する手段を提供しないために、やや悪です。 「this」を変更しないStructメソッドは、防御的なコピーを必要とせずに、読み取り専用の構造体でも呼び出すことができます。 「this」を変更するメソッドは、読み取り専用の構造体ではまったく呼び出せません。 .netは、「this」を変更しないstructメソッドが読み取り専用構造体で呼び出されることを禁止したくないが、読み取り専用構造体の変更を許可しないため、read-コンテキストのみで、おそらく両方の世界で最悪の状況に陥っています。

ただし、読み取り専用コンテキストでの自己変更メソッドの処理に関する問題にもかかわらず、可変構造体は、可変クラス型よりもはるかに優れたセマンティクスを提供することがよくあります。次の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); 

各方法について、次の質問に答えてください。

  1. メソッドが「安全でない」コードを使用しないと仮定すると、fooを変更する可能性がありますか?
  2. メソッドが呼び出される前に「foo」への外部参照が存在しない場合、外部参照は後に存在できますか?

回答:

質問1:
Method1():いいえ(明確な意図)
Method2():はい(意図をクリア)
Method3():はい(不確かな意図)
質問2:
Method1():いいえ
Method2():no (安全でない場合を除く)
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] = New RectClass(RectClassArray [321] ]); RectClassArray [321] .X = 555; "何をすべきかを知るには、RectClass(コピーコンストラクタ、コピー元メソッドなどをサポートしますか)と配列の使用目的の両方についてさらに多くを知る必要があります。構造体を使用するほどきれいな場所はありません。

確かに、残念ながら、配列以外のコンテナクラスが構造体配列のクリーンなセマンティクスを提供する良い方法はありません。コレクションにインデックスを付けたい場合などに最適な方法です。文字列は、おそらく、インデックスの文字列、汎用パラメータ、および汎用パラメータとコレクションアイテムの両方の参照によって渡されるデリゲートを受け入れる汎用「ActOnItem」メソッドを提供することになります。これにより、構造体配列とほぼ同じセマンティクスが可能になりますが、vb.netとC#の人々がNice構文を提供するように説得されない限り、コードは、たとえそれが適度なパフォーマンス(一般的なパラメーターを渡す場合でも)静的デリゲートの使用を許可し、一時クラスインスタンスを作成する必要を回避します)。

個人的には、私は憎しみのエリック・リッパー他に覗き見ています。可変の値の種類について説明します。それらは、あちこちで使用されている無差別参照型よりもはるかにクリーンなセマンティクスを提供します。 .netの値型のサポートにはいくつかの制限がありますが、可変の値型が他の種類のエンティティよりも適している場合が多くあります。

36
supercat

値型は基本的に不変の概念を表します。 Fx、整数、ベクトルなどの数学的な値を持ち、それを修正することはできません。それは、値の意味を再定義するようなものです。値の種類を変更する代わりに、別の一意の値を割り当てる方が合理的です。プロパティのすべての値を比較することで、値型が比較されるという事実を考えてください。ポイントは、プロパティが同じ場合、その値の同じ普遍的な表現であるということです。

Konradが言及しているように、値はその一意の時点を表し、状態またはコンテキスト依存性を持つ時間オブジェクトのインスタンスではないため、日付を変更することも意味がありません。

これがあなたにとって意味があることを願っています。確かに、実際の詳細よりも、値型でキャプチャしようとする概念についてです。

22

プログラマーの観点から予測できない動作を引き起こす可能性のある別のいくつかのコーナーケースがあります。ここにそれらのカップル。

  1. 不変の値型と読み取り専用フィールド
// 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);
    }
}
 </ code>
  1. 可変値タイプと配列

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);

したがって、あなたとチームの他のメンバーがあなたが何をしているかを明確に理解している限り、可変構造は悪ではありません。しかし、プログラムの振る舞いが予想される振る舞いと異なる場合は、あまりにも多くのコーナーケースがあり、微妙なエラーの生成と理解が困難になります。

17

C/C++などの言語でプログラミングしたことがある場合、構造体は可変として使用しても問題ありません。 refでそれらを渡すだけで、何も間違ったことはありません。私が見つける唯一の問題は、C#コンパイラの制限であり、場合によっては、愚かなことに強制的にコピーの代わりに構造体への参照を使用することができません(構造体がC#クラスの一部である場合のように) )。

したがって、可変構造体は悪ではなく、C#にはmade悪があります。私は常にC++で可変構造体を使用しており、非常に便利で直感的です。対照的に、C#を使用すると、オブジェクトの処理方法のために、構造体をクラスのメンバーとして完全に放棄することになりました。彼らの利便性は私たちにコストをかけました。

16
ThunderGr

構造体の目的(C#、Visual Basic 6、Pascal/Delphi、C++構造体型(またはクラス)でポインターとして使用されていない場合)に固執すると、構造体は化合物変数。つまり、共通名(メンバーを参照するレコード変数)の下で、変数のパックセットとしてそれらを扱います。

OOPに深く慣れている多くの人々を混乱させることはわかっていますが、正しく使用すれば、そのようなことは本質的に悪であると言う十分な理由はありません。一部の構造は意図したとおりに不変です(これはPythonのnamedtupleの場合です)が、考慮すべきもう1つのパラダイムです。

はい:構造体には多くのメモリが含まれますが、次の操作を行ってもメモリが正確に増えるわけではありません。

point.x = point.x + 1

に比べ:

point = Point(point.x + 1, point.y)

メモリ消費量は、少なくとも同じか、不変の場合はさらに大きくなります(ただし、その場合は、言語によっては現在のスタックでは一時的なものになります)。

しかし、最後に、構造は構造であり、オブジェクトではありません。 POOでは、オブジェクトの主なプロパティはidentityであり、ほとんどの場合、そのアドレスはメモリアドレス以下です。 Structはデータ構造(適切なオブジェクトではないため、IDを持たない)を表し、データを変更できます。他の言語では、recordstructの代わりに、 Pascalの場合)はWordであり、同じ目的を持ちます:ファイルから読み取られ、変更され、ファイルにダンプされることを意図したデータレコード変数のみです(主な用途であり、多くの言語では、レコード。ただし、適切に呼び出されたオブジェクトの場合は必ずしもそうではありません)。

良い例が必要ですか?構造体は、ファイルを簡単に読み取るために使用されます。 Pythonには このライブラリ があります。これは、オブジェクト指向であり、構造体をサポートしていないため、別の方法で実装する必要があるためです。構造体を実装する言語には、その機能が組み込まれています。 PascalやCなどの言語で適切な構造体を使用してビットマップヘッダーを読み取ってみてください(構造体が適切に構築および配置されている場合、Pascalではレコードベースのアクセスを使用せず、任意のバイナリデータを読み取る機能を使用します)。そのため、ファイルおよび直接(ローカル)メモリアクセスの場合、構造体はオブジェクトよりも優れています。今日に関しては、JSONとXMLに慣れているため、バイナリファイルの使用を忘れています(副作用として、構造体の使用も)。しかし、はい:それらは存在し、目的があります。

彼らは悪ではありません。正しい目的に使用してください。

ハンマーの観点から考えると、ネジを釘として扱い、ネジが壁に突入するのが難しくなり、ネジのせいであり、邪悪なものになります。

11
Luis Masuelli

1,000,000個の構造体の配列があるとします。 bid_price、offer_price(おそらく小数)などのようなものを持つエクイティを表す各構造体は、C#/ VBによって作成されます。

アンマネージヒープに割り当てられたメモリブロックに配列が作成され、他のネイティブコードスレッドが配列に同時にアクセスできるようになることを想像してください(おそらく、高パフォーマンスのコードが計算を実行します)。

C#/ VBコードが価格変更のマーケットフィードをリッスンしていることを想像してください。そのコードは配列の一部の要素(いずれのセキュリティ)にアクセスし、いくつかの価格フィールドを変更する必要があります。

これが毎秒数十回または数十万回行われていると想像してください。

事実に直面してみましょう。この場合、これらの構造体は実際に変更可能にする必要があります。他のネイティブコードと共有されているため、コピーを作成しても役に立たないためです。これらのレートで約120バイトの構造体のコピーを作成するのは、特に更新が実際に1〜2バイトだけに影響する場合は特に手間がかかるためです。

ヒューゴ

11
Hugo

何かを変異させることができるとき、それはアイデンティティの感覚を獲得します。

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のクローンを作成し、クローンを移動し、オリジナルを破棄するを考える方が自然です。どちらの操作でもeric.positionの内容を変更できますが、一方は他方よりも直感的です。同様に、Ericを変更するメソッドの(リファレンスとして)Ericを渡す方がより直感的です。メソッドにEricのクローンを与えることは、ほとんどの場合驚くべきことです。 Personを変更したい場合は、Personへの参照を要求することを忘れないでください。そうしないと、間違ったことをしてしまいます。

型を不変にすると、問題はなくなります。 ericを変更できない場合でも、ericを受け取るか、ericのクローンを受け取るかは関係ありません。より一般的には、観察可能な状態のすべてが次のいずれかのメンバーで保持されている場合、型は値で渡しても安全です。

  • 不変
  • 参照タイプ
  • 値渡ししても安全

これらの条件が満たされている場合、浅いコピーでもレシーバが元のデータを変更できるため、可変値タイプは参照タイプのように動作します。

不変のPersonの直感性は、何をしようとしているかによって異なります。 Personが人に関するデータのセットを表す場合、それについて直感的でないものは何もありません。 Person変数は、オブジェクトではなく抽象を真に表します。 (その場合、おそらくPersonDataに名前を変更する方が適切です。)Personが実際に人自身をモデリングしている場合、考えている落とし穴を避けたとしても、クローンを常に作成および移動するという考えは馬鹿げています。オリジナルを再修正します。その場合、単にPersonを参照型(つまり、クラス)にする方が自然でしょう。

確かに、関数型プログラミングが教えてくれたように、everything不変(誰もericへの参照を密かに保持して変異させることはできません)にすることには利点がありますが、それはOOPそれはあなたのコードで作業している他の誰にとってもまだ直観的ではありません。

7
Doval

個人的にコードを見ると、次のことが非常に不格好に見えます。

data.value.set(data.value.get()+ 1);

単にではなく

data.value ++;またはdata.value = data.value + 1;

データのカプセル化は、クラスを渡すときに役立ち、値が制御された方法で変更されるようにする場合に役立ちます。しかし、パブリックセットがあり、値をこれまでに渡された値に設定する以上のことをしない関数を取得する場合、単純にパブリックデータ構造を渡すことよりも改善されますか?

クラス内にプライベート構造を作成するとき、その構造を作成して、一連の変数を1つのグループに編成しました。その構造のコピーを取得して新しいインスタンスを作成するのではなく、クラススコープ内でその構造を変更できるようにします。

私にとってこれは、パブリック変数を整理するために使用される構造の有効な使用を妨げます。アクセス制御が必要な場合は、クラスを使用します。

5
Mike

Eric Lippert氏の例にはいくつかの問題があります。構造体がコピーされるポイントと、注意しないとどのように問題になるかを説明するために工夫されています。この例を見ると、プログラミングの悪さの結果であり、実際には構造体やクラスの問題ではありません。

  1. 構造体にはパブリックメンバーのみが含まれることになっているため、カプセル化は必要ありません。もしそうなら、それは本当にタイプ/クラスでなければなりません。同じことを言うのに、2つの構成要素は本当に必要ありません。

  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());
    }

Struct自体の問題ではなく、プログラミングの習慣の問題のようです。構造体は可変であると想定されています。それがアイデアと意図です。

出来上がりの変更の結果は期待どおりに動作します。

1 2 3任意のキーを押して続行します。 。 。

5
Ramesh Kadambi

構造体とは何の関係もありません(C#とも関係ありません)が、Javaでは、可変オブジェクトに問題がある場合があります。ハッシュマップのキー。それらをマップに追加した後にそれらを変更し、その ハッシュコード を変更すると、悪事が起こる可能性があります。

5
Bombe

可変データには多くの長所と短所があります。 100万ドルの欠点はエイリアシングです。同じ値が複数の場所で使用されており、そのうちの1つがそれを変更した場合、それを使用している他の場所に魔法のように変更されたように見えます。これは、競合状態に関連していますが、同一ではありません。

100万ドルの利点は、モジュール性です。可変状態を使用すると、変更情報を知る必要のないコードから変更情報を隠すことができます。

通訳の芸術 は、これらのトレードオフをある程度詳しく説明し、いくつかの例を示します。

4
Andru Luvisi

正しく使えば悪だとは思わない。実動コードには入れませんが、構造体の単体テストモックなど、構造体の寿命が比較的短いものには使用します。

Ericの例を使用して、おそらくそのEricの2番目のインスタンスを作成しますが、テストの性質(つまり、複製、変更)に応じて調整を行います。テストの比較として使用する予定がない限り、テストスクリプトの残りの部分でEric2を使用している場合は、Ericの最初のインスタンスで何が起こるかは関係ありません。

これは、特定のオブジェクト(構造体のポイント)を浅く定義するレガシーコードをテストまたは変更するのに最も役立ちますが、不変の構造体を使用することで、迷惑な使用を防ぎます。

1
ShaunnyBwoy