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


485

ここでの議論に続いて、可変構造体は「悪」である(この質問への回答のように)とのコメントを何度か読みました。

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


12
可変構造体が悪であると主張することは、可変ints、boolsであると主張することに似ており、他のすべての値タイプは悪です。可変性と不変性の場合があります。これらのケースは、メモリの割り当て/共有のタイプではなく、データが果たす役割に依存します。
Slipp D. Thompson 2014

41
@slippでint、変更可能でboolはありません
。– Blorgbeardは、2015

2
.-syntax。参照型データと値型データを使用した操作は、明確に異なっていても同じに見えます。これは、構造体ではなくC#のプロパティの欠点です。一部の言語では、a[V][X] = 3.14インプレースで変更するための代替構文が提供されています。C#では、 'MutateV(Action <ref Vector2> mutator) `のようなstruct-memberミューテーターメソッドを提供し、それを次のように使用する方がよいでしょうa.MutateV((v) => { v.X = 3; }) (例は、C#がrefキーワードに関して持っている制限のために過度に単純化されていますが、回避策が可能なはずです)
Slipp D. Thompson

2
@スリップまあ、私はこれらの種類の構造体について正反対だと思います。DateTimeやTimeSpanなどの.NETライブラリにすでに実装されている構造体が不変であると思いますか?そのような構造体のvarの1つのメンバーのみを変更することは有用かもしれませんが、それはあまりにも不便であり、あまりに多くの問題を引き起こします。C#はアセンブラーにコンパイルされないため、ILにコンパイルされるため、実際にはプロセッサーの計算が間違っています。ILでは、(という名前の変数が既にある場合x)この単一の操作は4つの命令です:ldloc.0(0インデックス変数を...にロードします
Sushi271

1
...タイプ。Tタイプです。Refは、変数をメソッド自体に渡すための単なるキーワードであり、そのコピーではありません。また、変数を変更できるため、参照タイプにも意味があります。つまり、メソッド外での参照は、メソッド内で変更された後、他のオブジェクトを指します。ref Tはタイプではないため、メソッドパラメータを渡す方法なので、に入れることはできません<>。原因は、タイプのみを置くことができるためです。したがって、それは正しくありません。多分そうするのが便利かもしれません、多分C#チームが新しいバージョンのためにこれを作ることができるかもしれませんが、現在彼らはいくつかに取り組んでいます...
Sushi271

回答:


290

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

したがって、コピーを変更する場合は、そのコピーのみを変更し、元のコピーや周りにある他のコピーは変更しません。

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

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


82
「構造体が不変の場合、すべてのコピーは同じになります。」いいえ、別の値が必要な場合は、意識的にコピーを作成する必要があることを意味します。これは、オリジナルを変更していると思っても、コピーを変更しても捕まらないことを意味します。
ルーカス

19
@Lucas私はあなたが別の種類のコピーについて話していると思います私は価値によって渡された結果として作成された自動コピーについて話している実際にはコピーではなく、異なるデータを含む意図的な新しいインスタントです。
トランプスター

3
あなたの編集(16か月後)は、それを少し明確にします。ただし、「(不変の構造体)は、元のファイルを変更していると思ってコピーを変更しても、捕まらないことを意味します」と、まだ待機しています。
Lucas、

6
@Lucas:構造体のコピーを作成し、それを変更し、何らかの形でオリジナルを変更していると考える危険性(構造体フィールドを書き込んでいるという事実が、自分のコピーのみを書き込んでいるという事実を自覚する場合)そこに含まれる情報を保持する手段としてクラスオブジェクトを保持している誰かがオブジェクトを変更して自身の情報を更新し、その過程で他のオブジェクトが保持している情報を破壊する危険性と比べるとかなり小さいです。
スーパーキャット'28

2
3番目の段落は、よくても、不明瞭に聞こえます。構造体が不変の場合、そのフィールドや作成されたコピーのフィールドを変更することはできません。「あなたはそれを変更したい場合はあなたが...持っている」あまりにも誤解だと、あなたが変更できないことを 、これまで、どちらも意識的にも無意識のうちに。必要なデータを含む新しいインスタンスの作成は、同じデータ構造を持つこと以外は、元のコピーとは関係ありません。
Saeb Amini、2016

167

どこから始めるか;-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の、stringDateTimeそしてbool、あなたは非常に迅速に、多くのメモリを通じて書き込むことができます。クラスを使用すると、複数の呼び出し元が同じインスタンスへの参照を共有できます(参照は小さい)。


5
ええ、そうですが、コンパイラはそのように愚かです。property-structメンバーへの割り当てを許可しないこと++オペレーターに許可されているため、愚かな設計決定でした。この場合、コンパイラーはプログラマーを煩わせるのではなく、明示的な割り当て自体を書き込むだけです。
コンラートルドルフ

12
@Konrad:myObj.SomeProperty.Size = 22は、myObj.SomePropertyのCOPYを変更します。コンパイラは明らかなバグからあなたを救っています。また、++では許可されていません。
ルーカス

@Lucas:まあ、Mono C#コンパイラは確かにそれを許可します–私はWindowsを持っていないので、Microsoftのコンパイラをチェックできません。
Konrad Rudolph

6
@Konrad-間接性が1つ少ないため、機能するはずです。ブロックされるのは、「スタック上に一時的な値としてのみ存在し、蒸発してゼロになるものの値を変更すること」です。
Marc Gravell

2
@Marc Gravell:前のコードでは、List [0]に影響を与えずに、名前が "abc"で、他の属性がList [0]である "Foo"になります。Fooがクラスの場合、それを複製してからコピーを変更する必要があります。私の考えでは、値タイプとクラスの違いに関する大きな問題は、「。」の使用です。2つの目的のための演算子。もし私がドラチャーを持っているなら、クラスは両方をサポートすることができます メソッドとプロパティは「->」ですが、「。」は通常のセマンティクスです。プロパティは、適切なフィールドが変更された新しいインスタンスを作成することです。
supercat

71

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

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

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


1
エリック・リッパートはそうだと言います...私の答えを見てください
Marc Gravell

42
私がエリック・リッペルトを尊敬しているように、彼は神ではありません(あるいは少なくともまだです)。リンクしているブログ投稿と上記の投稿は、構造体を不変にするための当然の議論ですが、実際に、可変構造体を決して使用しないことについて非常に弱いです。ただし、この投稿は+1です。
スティーブンマーティン

2
C#で開発する場合、通常は時々可変性が必要になります。特に、ビジネスモデルでは、ストリーミングなどを既存のソリューションとスムーズに連携させる必要があります。:私は可変と不変のデータで動作するように、可変性の周りにほとんどの問題を解決(私は願って)方法についての記事を書いたrickyhelgesson.wordpress.com/2012/07/17/...
リッキーHelgesson

3
@StephenMartin:多くの場合、単一の値をカプセル化する構造体は不変である必要がありますが、構造体は、「同一性」を持たない独立しているが関連する変数(点のX座標とY座標など)の固定セットをカプセル化するための最良の媒体です。グループ。その目的で使用される構造体は一般に変数をパブリックフィールドとして公開する必要があります。そのような目的のために構造体よりもクラスを使用するほうが適切であるという考えは、まったく間違っていると考えます。多くの場合、不変クラスは効率が悪く、可変クラスは恐ろしいセマンティクスを持っています。
スーパーキャット2013年

2
@StephenMartin:たとえば、floatグラフィックス変換の6つのコンポーネントを返すことになっているメソッドまたはプロパティを考えてみます。このようなメソッドが6つのコンポーネントを持つ公開フィールド構造体を返す場合、構造体のフィールドを変更しても、受信元のグラフィックスオブジェクトは変更されないことは明らかです。このようなメソッドが変更可能なクラスオブジェクトを返す場合、そのプロパティを変更すると、基になるグラフィックスオブジェクトが変更され、変更されない可能性があります-実際にはわかりません。
スーパーキャット2013年

48

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

これらは、高パフォーマンスの状況では絶対に必要です。たとえば、キャッシュラインやガベージコレクションがボトルネックになる場合などです。

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

C#の構文では値型または参照型のメンバーのアクセスを区別するのに役立たないという点がわかります。そのため、私はすべて、可変構造よりも不変を強制する不変構造を好んでいます。

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

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


8
また、変数のセットをダクトテープと一緒に固定して、それらの値を個別にまたは1つの単位として処理または保存できるようにする場合は、コンパイラーに固定セットを固定するように依頼する方がはるかに理にかなっていると思います。変数を一緒に(つまりstruct、パブリックフィールドで宣言します)、使用できるクラスを定義するよりも、同じ目的を達成するために、または構造体に一連のジャンクを追加して、そのようなクラスをエミュレートするようにします(それを持たせるのではなく)ダクトテープで固定された変数のセットのように動作します。これは、そもそも本当に必要なものです)
supercat

41

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

「プロパティ」を変更する構造体メソッド(プロパティセッターとは異なります)は、.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. メソッドが「安全でない」コードを使用しない場合、fooを変更する可能性がありますか?
  2. メソッドが呼び出される前に 'foo'への外部参照が存在しない場合、外部参照が後に存在する可能性はありますか?

答え:

質問1:
Method1():なし(意図クリア)
Method2():はい(意図クリア)
Method3():はい(不確かな意図)
質問2:
Method1():いいえ
Method2():いいえ(危険な場合を除き)
Method3():はい

Method1はfooを変更できず、参照を取得しません。Method2は、一時的なfooへの参照を取得します。これは、fooのフィールドを変更するまで何度でも、任意の順序で変更できますが、その参照を永続化することはできません。Method2が戻る前は、安全でないコードを使用していない限り、「foo」参照から作成された可能性のあるすべてのコピーが消えています。Method3は、Method2とは異なり、無差別に共有可能なfooへの参照を取得します。これを使用して何ができるかはわかりません。それはfooをまったく変更しないかもしれません、それはfooを変更してから戻るかもしれません。

構造の配列は素晴らしいセマンティクスを提供します。Rectangle型のRectArray [500]が与えられた場合、たとえば、要素123を要素456にコピーし、しばらくしてから要素123の幅を要素456に影響を与えずに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#の人々が素晴らしい構文を提供するように追求されない限り、コードは、それが合理的なパフォーマンスであっても不格好に見えるでしょう(一般的なパラメーターを渡すと静的デリゲートの使用を許可し、一時クラスインスタンスを作成する必要がなくなります)。

個人的に、私は憎しみのあるエリック・リッペルトらに腹を立てています。変更可能な値の型に関する説明。それらは、至る所で使用されている無差別参照タイプよりもはるかに明確なセマンティクスを提供します。.netの値タイプのサポートにはいくつかの制限がありますが、変更可能な値タイプが他の種類のエンティティよりも適している場合が多くあります。


1
@Ron Warholic:SomeRectが四角形であることは明らかではありません。Rectangleから暗黙的に型キャストできる他の型の場合もあります。ただし、Rectangleから暗黙的に型キャストできる唯一のシステム定義型はRectangleFであり、RectangleFのフィールドをRectangleのコンストラクターに渡そうとすると、コンパイラーは不意に動作します(前者はSingle、後者はIntegerであるため)。 、そのような暗黙の型キャストを許可するユーザー定義の構造体がある可能性があります。ところで、SomeRectがRectangleでもRectangleFでも、最初のステートメントは同じように機能します。
スーパーキャット'28年

あなたが示したのは、不自然な例では、1つの方法がより明確であると信じているということだけです。私があなたの例を挙げればRectangle、あなたが非常に不明確な行動をするという一般的な状況を簡単に思いつくかもしれません。WinForms RectangleがフォームのBoundsプロパティで使用される変更可能な型を実装していることを考慮してください。境界を変更したい場合は、素敵な構文を使用したいと思います。form.Bounds.X = 10;ただし、これはフォーム上でまったく何も変更しません(そして、そのことを通知する素敵なエラーを生成します)。不整合はプログラミングの悩みの種であり、不変性が望まれる理由です。
Ron Warholic、2011年

3
@Ron Warholic:ところで、"form.Bounds.X = 10;"と言えるようにしたいと思います。動作させるだけですが、システムはそのためのクリーンな方法を提供していません。値タイプのプロパティをコールバックを受け入れるメソッドとして公開するための規約は、クラスを使用するどのアプローチよりもはるかにクリーンで効率的で確実に正しいコードを提供できます。
スーパーキャット'28年

4
この回答は、投票数の多い回答のいくつかよりもはるかに洞察に富んでいます。可変値型に対する議論は、エイリアシングとミューテーションを組み合わせたときに「期待どおり」が発生するという概念に依存しているというのは、ばかげたことです。とにかくそれはひどいことです!
Eamon Nerbonne、2016年

2
@supercat:C#7について話しているref-return機能がそのベースをカバーしているかもしれません(私は実際には詳細に調べていませんが、表面的には同じように聞こえます)。
Eamon Nerbonne 2016年

24

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

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

これがあなたにとって意味のあることだと思います。確かに、実際の詳細ではなく、値タイプを使用してキャプチャしようとするコンセプトについてです。


まあ、それら少なくとも;-p
Marc Gravell

3
まあ、私はそれらがSystem.Drawing.Pointを不変にしたかもしれないと思いますが、それは深刻な設計エラーであったでしょう。ポイントは実際には典型的な値タイプであり、変更可能だと思います。そして、彼らは本当に初期のプログラミング101初心者を超えて誰にとっても問題を引き起こしません。
スティーブンマーティン

3
原則として、ポイントも不変である必要があると思いますが、そのタイプが使用するのを難しくしたり、エレガントさに欠ける場合は、もちろん、それも考慮する必要があります。誰も使用したくない場合は、最高の原則を支持するコード構成を使用しても意味がありません;)
Morten Christiansen

3
値型は、単純な不変の概念を表すのに役立ちますが、公開フィールド構造は、関連するが独立した値(点の座標など)の小さな固定セットを保持または渡すために使用するのに最適な型です。そのような値タイプの格納場所は、そのフィールドの値のみをカプセル化します。対照的に、可変参照型の格納場所は、可変オブジェクトの状態を保持する目的で使用できますが、同じオブジェクトに存在するユニバース全体の他のすべての参照のIDもカプセル化します。
スーパーキャット2013年

4
「値型は基本的に不変の概念を表します」。 いいえ、ありません。値型変数の最も古くて最も有用なアプリケーションの1つはintイテレータです。イテレータは、不変である場合はまったく役に立ちません。「値型のコンパイラ/ランタイム実装」を「値型に型付けされた変数」と混同していると思います。後者は、可能な値のいずれにも変更可能です。
Slipp D. Thompson

19

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

不変の値タイプと読み取り専用フィールド

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

したがって、変更可能な構造体は、あなたとチームの他のメンバーがあなたが何をしているかを明確に理解している限り、悪ではありません。しかし、プログラムの動作が予想とは異なるコーナーケースが多すぎて、微妙な生成やエラーの理解が困難になる可能性があります。


5
.net言語の値型のサポートがわずかに優れている場合、何が起こるかは、明示的に宣言されていない限り、構造体メソッドで「this」を変更することを禁止し、宣言されたメソッドを読み取り専用で禁止することです。コンテキスト。可変構造の配列は、他の手段では効率的に達成できない有用なセマンティクスを提供します。
スーパーキャット2009

2
これらは、可変構造体から発生する非常に微妙な問題の良い例です。私はこのような行動を期待していなかったでしょう。配列が参照を提供するのに、インターフェイスは値を提供するのはなぜですか?私は、いつでも価値観(私が実際に期待していること)を除いて、それが少なくとも他の方法であると考えていたでしょう。値を与える配列...
Dave Cousineau

@Sahuagin:残念ながら、インターフェースが参照を公開できる標準的なメカニズムはありません。特別に定義することによって、そのような事は安全かつ有効(例えば行うことができる可能性があり、.NETの方法があります「配列リファレンス<T>」を含む構造体T[]と整数のインデックスを、そして型のプロパティへのアクセスがあること提供するArrayRef<T>と解釈されます適切な配列要素へのアクセス)[クラスがArrayRef<T>他の目的のためにを公​​開したい場合は、それを取得するために、プロパティではなくメソッドを提供できます]。残念ながら、そのような規定はありません。
スーパーキャット2012年

2
ああ、これは...可変構造体を酷くする!
nawfal 2013年

1
はっきりしない非常に貴重な情報が含まれているため、この回答が好きです。しかし、実際には、これは、一部の主張のように、変更可能な構造体に対する議論ではありません。はい、私たちがここで目にするのは、エリックが言うように「絶望の落とし穴」ですが、この絶望の原因は可変性ではありません。絶望の原因は、構造体の自己変異メソッドです。(配列とリストの動作が異なる理由は、一方が基本的にメモリアドレスを計算する演算子であり、もう一方がプロパティであるためです。一般に、「参照」がアドレスであることを理解すると、すべて明らかになります。)
AnorZaken

18

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

したがって、変更可能な構造体は悪ではなく、C#によって悪になっています。私は常にC ++で可変構造体を使用しており、それらは非常に便利で直感的です。対照的に、C#では、構造体がオブジェクトを処理する方法が原因で、構造体をクラスのメンバーとして完全に破棄するようになりました。彼らの利便性は私たちのものを犠牲にしました。


確かにいくつかの制限がありますが、構造タイプのクラスフィールドを持つことは、たいてい非常に役立つパターンです。フィールドではなくプロパティを使用する場合、またはを使用する場合はパフォーマンスが低下しますreadonlyが、これらのことを回避する場合は、構造タイプのクラスフィールドで十分です。構造体の唯一の基本的な制限は、変更可能なクラスタイプの構造体フィールドがint[]IDまたは不変の値のセットをカプセル化できるが、不要なIDもカプセル化せずに変更可能な値をカプセル化するために使用できないことです。
スーパーキャット2013年

13

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

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

はい:構造体には大量のメモリが含まれますが、次のようにすると、メモリが正確に増えるわけではありません。

point.x = point.x + 1

に比べ:

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

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

しかし、最後に、構造はオブジェクトではなく構造です。POOでは、オブジェクトの主要なプロパティはそれらのIDであり、ほとんどの場合、それはメモリアドレス以下です。Structはデータ構造を表しており(適切なオブジェクトではないため、IDはありません)、データを変更できます。他の言語では、(Pascalの場合のようにstructではなく)recordが単語であり、同じ目的を保持します。ファイルから読み取られ、変更され、ファイルにダンプされることを目的とするデータレコード変数(メイン多くの言語では、レコードのデータ配置を定義することもできますが、適切に呼び出されたオブジェクトの場合は必ずしもそうではありません)。

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

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

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


12

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

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

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

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

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

ヒューゴ


3
真ですが、この場合、構造体を使用する理由は、そうすることは外部の制約(ネイティブコードの使用による制約)によってアプリケーションの設計に課されるためです。これらのオブジェクトについて説明する他のすべてのことは、それらが明らかにC#またはVB.NETのクラスである必要があることを示唆しています。
Jon Hanna

6
なぜクラスオブジェクトである必要があると考える人がいるかはわかりません。すべての配列スロットに参照の異なるインスタンスが入力されている場合、クラスタイプを使用すると、メモリ要件に12バイトまたは24バイトが追加され、クラスオブジェクト参照の配列への順次アクセスは、順次アクセスよりもはるかに遅くなる傾向があります。構造体の配列。
スーパーキャット2013年

9

何かが変異する可能性があるとき、それはアイデンティティの感覚を得ます。

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、1つは他よりも直感的です。同様に、Ericを変更する方法について(参照として)Ericを渡す方が直感的です。メソッドをエリックのクローンに与えることは、ほとんど常に驚くべきことです。変異を起こしたい人Personは、参照を要求することを忘れないでくださいPerson。そうしないと、間違ったことが行われます。

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

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

これらの条件が満たされると、浅いコピーでも受信者が元のデータを変更できるため、可変値型は参照型のように動作します。

イミュータブルの直感性は、Person何をしようとしているかに依存します。人に関する一連のデータをPerson表すだけの場合、直感的なものはありません。変数は、オブジェクトではなく、実際に抽象的な値を表します。(その場合は、名前をに変更する方がおそらく適切です。)が実際に個人をモデル化している場合、クローンを絶えず作成および移動するという考えは、変更していると考える落とし穴を回避したとしても、ばかげています。オリジナル。その場合は、単に参照型(つまり、クラス)を作成する方が自然でしょう。PersonPersonDataPersonPerson

確かに、関数型プログラミングはすべてを不変にすることには利点があります(だれもeric彼への参照をこっそり保持して変更することはできません)。コード。


3
アイデンティティについてのあなたのポイントは良いものです。IDは、何かへの複数の参照が存在する場合にのみ関連することに注意してください。場合はfoo、宇宙でのターゲットの任意の場所への参照のみを保持しており、何もオブジェクトのアイデンティティ・ハッシュ値は、そのフィールドを変異することをキャプチャしていないfoo.X、意味的に行うことと等価であるfooだけで、それは以前と呼ばもののようである新しいオブジェクトにポイントを、しかしでX目的の値を保持します。クラス型の場合、一般に何かへの複数の参照が存在するかどうかを知るのは困難ですが、構造体の場合は簡単です。それらは存在しません。
スーパーキャット2013年

1
場合はThing変更可能なクラス型で、Thing[] オブジェクトのアイデンティティをカプセル化- 1はそれがないか望んでいるかどうか- 1は何があることを保証することはできませんしない限り、Thing任意の外部の参照が存在するに配列で、これまで変異されます。配列要素にIDをカプセル化させたくない場合は、一般に、参照を保持するアイテムが変更されないこと、または保持するアイテムへの外部参照が存在しないことを確認する必要があります[ハイブリッドアプローチも機能する]。どちらの方法も非常に便利ではありません。Thingが構造体の場合、a Thing[]は値のみをカプセル化します。
スーパーキャット2013年

オブジェクトの場合、そのIDは場所から取得されます。参照型のインスタンスは、メモリ内の場所のおかげでIDを持ち、データではなくID(参照)のみを渡しますが、値型は格納されている外部の場所でIDを持ちます。エリック値タイプのIDは、彼が格納されている変数からのみ取得されます。あなたが彼を通り過ぎると、彼はアイデンティティを失うでしょう。
IllidanS4はモニカを2014

6

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


3
マップでキーとしてクラスを使用する場合も同様です。
Marc Gravell

6

個人的にコードを見ると、以下はかなり不格好に見えます。

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

単にではなく

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

データのカプセル化は、クラスを渡し、値が制御された方法で変更されるようにする場合に便利です。ただし、パブリックセットを使用して、渡されたものに値を設定するだけの関数を取得する場合、パブリックデータ構造体を単に渡すだけの場合よりも、どのように改善されますか?

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

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


1
要点はまっすぐ!構造は、アクセス制御制限のない組織単位です!残念ながら、C#はこの目的のためにそれらを役に立たなくしました!
ThunderGr 2013年

どちらの例も変更可能な構造体を示しているため、これは完全にポイントを逃しています。
vidstige 2015

これは構造の目的ではないため、C#はこの目的のためにそれらを役に立たなくしました
Luiz Felipe

6

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

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

変更の結果、期待どおりに動作します。

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


4
小さな不透明な構造を不変のクラスオブジェクトのように動作するように設計しても問題はありません。オブジェクトのように動作するものを作成しようとする場合、MSDNガイドラインは妥当です。構造体は、オブジェクトのように動作する軽量なものが必要な場合や、ダクトテープで固定された一連の変数が必要な場合に適しています。しかし、何らかの理由で、多くの人々は、構造には2つの異なる使用法があり、一方に適切なガイドラインは他方には不適切であることを認識していません。
スーパーキャット2015年

5

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

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

Art of the Interpreterでは、これらのトレードオフについて詳細に説明し、いくつかの例を示します。


構造体はc#ではエイリアスされません。すべての構造体の割り当てはコピーです。
2010

@recursive:場合によっては、それが可変構造体の主な利点であり、構造体は可変であってはならないという概念に疑問を投げかけるものです。コンパイラーが構造体を暗黙的にコピーする場合があるという事実は、可変構造体の有用性を低下させません。
supercat

1

正しく使えば悪だとは思いません。量産コードには入れませんが、構造化ユニットテストのモックのように、構造体の寿命が比較的短いものを使用します。

Ericの例を使用して、おそらくそのEricの2番目のインスタンスを作成しますが、それはテストの性質(つまり、複製してから変更)であるため、調整を行います。テストの比較としてEric2を使用する予定がない限り、テストスクリプトの残りの部分にEric2を使用しているだけでは、Ericの最初のインスタンスがどうなるかは問題ではありません。

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


私が見ているように、構造体はその中心にダクトテープで貼り付けられた変数の束です。.NETでは、構造体がダクトテープでくっついた変数の束以外のもののふりをしている可能性があり、実際には、くっついた変数の束以外のふりをするタイプのふりをしようとしているタイプをお勧めしますダクトテープを使用すると、統合されたオブジェクトとして動作する必要があります(構造体の場合は不変性を意味します)。プロダクションコードでさえ、型がある方がよいと
思い

...「各フィールドには最後に書き込まれたものが含まれている」以上のセマンティクスがないことは明らかであり、構造体を使用するコードにすべてのセマンティクスをプッシュします。考えると、例えば、Range<T>メンバーとの種類MinimumMaximum型のフィールドT、およびコードはRange<double> myRange = foo.getRange();、何についての保証MinimumMaximum含まれているから来る必要がありますfoo.GetRange();。持っRange露出フィールドの構造体であることは、それ自身のいずれかの動作を追加するつもりはないことが明らかになるだろう。
スーパーキャット2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.