C# でクラスではなく構造体を使用する必要があるのはいつですか? 私の概念モデルは、項目が単に値型のコレクションである場合に構造体が使用されるというものです。それらを論理的にまとめてまとまりのある全体にする方法。
ここでこれらのルールに出くわしました:
- 構造体は単一の値を表す必要があります。
- 構造体のメモリ フットプリントは 16 バイト未満である必要があります。
- 作成後に構造体を変更しないでください。
これらの規則は機能しますか? 構造体は意味的に何を意味しますか?
OPが参照するソースにはある程度の信頼性があります...しかし、Microsoftはどうですか-構造体の使用に関するスタンスは何ですか? Microsoft から追加の学習を求めたところ、次のことがわかりました。
型のインスタンスが小さく、一般的に寿命が短い場合、または一般的に他のオブジェクトに埋め込まれている場合は、クラスではなく構造体を定義することを検討してください。
型が次の特性をすべて備えていない限り、構造体を定義しないでください。
- プリミティブ型 (integer、double など) と同様に、単一の値を論理的に表します。
- インスタンス サイズは 16 バイト未満です。
- 不変です。
- 頻繁に箱詰めする必要はありません。
さて、#2 と #3 です。私たちの最愛の辞書には 2 つの内部構造体があります。
[StructLayout(LayoutKind.Sequential)] // default for structs
private struct Entry //<Tkey, TValue>
{
// View code at *Reference Source
}
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator :
IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable,
IDictionaryEnumerator, IEnumerator
{
// View code at *Reference Source
}
※参考元
「JonnyCantCode.com」ソースは 4 点満点中 3 点でした。#4 はおそらく問題にならないので、かなり許されます。構造体をボックス化していることに気付いた場合は、アーキテクチャを再考してください。
Microsoft がこれらの構造体を使用する理由を見てみましょう。
Entry
およびEnumerator
は、単一の値を表します。Entry
Dictionary クラスの外部でパラメーターとして渡されることはありません。さらに調査すると、IEnumerable の実装を満たすために、Dictionary はEnumerator
、列挙子が要求されるたびにコピーする構造体を使用することがわかります。Enumerator
Dictionary は列挙可能であり、IEnumerator インターフェースの実装 (IEnumerator ゲッターなど) への同等のアクセシビリティーが必要であるため、public です。 更新- さらに、構造体がインターフェイスを実装する場合 (Enumerator のように)、その実装された型にキャストされると、構造体は参照型になり、ヒープに移動されることに注意してください。Dictionary クラスの内部である Enumeratorは、依然として値型です。ただし、メソッドが を呼び出すとすぐにGetEnumerator()
、参照型IEnumerator
が返されます。
ここに表示されていないのは、構造体を不変に保つ、または 16 バイト以下のインスタンス サイズのみを維持するという要件の試みまたは証拠です。
readonly
-不変ではありませんEntry
未定の有効期間 (からAdd()
、からRemove()
、Clear()
またはガベージ コレクション) があります。そして... 4. 両方の構造体が TKey と TValue を格納します。これらは参照型として使用できることがよく知られています (ボーナス情報を追加)。
ハッシュ化されたキーにもかかわらず、構造体のインスタンス化は参照型よりも高速であるため、辞書は部分的に高速です。ここでは、Dictionary<int, int>
300,000 個のランダムな整数を格納し、キーが順次インクリメントされます。
容量: 312874
MemSize: 2660827 バイト
完了したサイズ変更: 5
ミリ秒 いっぱいになるまでの合計時間: 889 ミリ秒
容量: 内部配列のサイズを変更する前に使用できる要素の数。
MemSize : ディクショナリを MemoryStream にシリアル化し、バイト長を取得することによって決定されます (この目的には十分正確です)。
サイズ変更の完了: 内部配列のサイズを 150862 要素から 312874 要素に変更するのにかかる時間。各要素が を介してシーケンシャルにコピーされるArray.CopyTo()
ことを考えると、それはそれほど粗末ではありません。
埋めるまでの合計時間OnResize
: ロギングとソースに追加したイベントにより、確かにゆがめられています。ただし、操作中に 15 回のサイズ変更を行いながら、300k の整数を埋めることは依然として印象的です。好奇心からですが、容量がわかっている場合は、合計でどれくらいの時間がかかるでしょうか? 13ms
では、Entry
クラスだったらどうでしょうか。これらの時間またはメトリックは、実際にそれほど異なるでしょうか?
容量: 312874
MemSize: 2660827 バイト
完了したサイズ変更: 26 ミリ秒
いっぱいになるまでの合計時間: 964 ミリ秒
明らかに、大きな違いはサイズ変更にあります。ディクショナリが容量で初期化されている場合、違いはありますか? 気にするほどではありません... 12ms。
は構造体であるためEntry
、参照型のような初期化は必要ありません。これは、値型の美しさでもあり、悩みの種でもあります。参照型として使用するEntry
には、次のコードを挿入する必要がありました。
/*
* Added to satisfy initialization of entry elements --
* this is where the extra time is spent resizing the Entry array
* **/
for (int i = 0 ; i < prime ; i++)
{
destinationArray[i] = new Entry( );
}
/* *********************************************** */
の各配列要素をEntry
参照型として初期化する必要があった理由は、MSDN: Structure Designにあります。要するに:
構造体の既定のコンストラクターを提供しないでください。
構造体で既定のコンストラクターが定義されている場合、構造体の配列が作成されると、共通言語ランタイムは、各配列要素に対して既定のコンストラクターを自動的に実行します。
C# コンパイラなどの一部のコンパイラでは、構造体に既定のコンストラクタを持たせることができません。
それは実際には非常に単純で、アシモフの3 つのロボット工学の法則から借用します。
...これから何を学ぶべきか: 要するに、値型の使用に責任を持つことです。それらは迅速かつ効率的ですが、適切に管理されていないと、多くの予期しない動作 (つまり、意図しないコピー) を引き起こす可能性があります。
あなたがいつでも:
ただし、構造体 (任意に大きい) は、クラス参照 (通常は 1 つの機械語) よりも渡すのにコストがかかるため、実際にはクラスの方が高速になる可能性があることに注意してください。
元の投稿に記載されているルールに同意しません。ここに私のルールがあります:
配列に格納する場合は、パフォーマンスのために構造体を使用します。(構造体が答えになるのはいつですか?も参照してください) 。
C/C++ との間で構造化データを渡すコードでそれらが必要です
構造体は、必要でない限り使用しないでください。
参照セマンティクスではなく値セマンティクスが必要な場合は、構造体を使用します。
なぜ人々がこれに反対票を投じているのかはわかりませんが、これは有効な点であり、op が彼の質問を明確にする前に作成されたものであり、構造体の最も基本的な理由です。
参照セマンティクスが必要な場合は、構造体ではなくクラスが必要です。
「それは値です」という答えに加えて、構造体を使用する特定のシナリオの 1 つは、ガベージ コレクションの問題を引き起こしているデータのセットがあり、多くのオブジェクトがあることがわかっている場合です。たとえば、 Person インスタンスの大規模なリスト/配列。ここでの自然な比喩はクラスですが、多数の長期の Person インスタンスがある場合、それらは GEN-2 を詰まらせ、GC ストールを引き起こす可能性があります。シナリオがそれを保証する場合、ここで考えられるアプローチの 1 つは、 Person 構造体の配列 (リストではない) を使用することですPerson[]
。現在、GEN-2 に何百万ものオブジェクトを持つ代わりに、LOH に単一のチャンクがあります (ここでは文字列などは想定していません。つまり、参照のない純粋な値です)。これによる GC への影響はほとんどありません。
このデータを扱うのは厄介です。なぜなら、データはおそらく構造体に対してサイズが大きすぎるためであり、常に脂肪値をコピーしたくないからです。ただし、配列内で直接アクセスしても、構造体はコピーされません。構造体はインプレースです (コピーするリスト インデクサーとは対照的です)。これは、インデックスに関する多くの作業を意味します。
int index = ...
int id = peopleArray[index].Id;
値自体を不変にしておくと、ここで役立つことに注意してください。より複雑なロジックの場合は、by-ref パラメータを持つメソッドを使用します。
void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);
繰り返しますが、これはインプレースです - 値をコピーしていません。
非常に特殊なシナリオでは、この戦術は非常に成功する可能性があります。ただし、これはかなり高度なシナリオであり、何をしているのか、またその理由がわかっている場合にのみ試してください。ここでのデフォルトはクラスです。
C# 言語仕様から:
1.7 構造体
クラスと同様に、構造体はデータ メンバーと関数メンバーを含むことができるデータ構造ですが、クラスとは異なり、構造体は値型であり、ヒープ割り当てを必要としません。構造体型の変数は構造体のデータを直接格納しますが、クラス型の変数は動的に割り当てられたオブジェクトへの参照を格納します。構造体型はユーザー指定の継承をサポートせず、すべての構造体型は型オブジェクトから暗黙的に継承します。
構造体は、値のセマンティクスを持つ小さなデータ構造に特に役立ちます。複素数、座標系の点、またはディクショナリのキーと値のペアはすべて、構造体の良い例です。小さなデータ構造にクラスではなく構造体を使用すると、アプリケーションが実行するメモリ割り当ての数に大きな違いが生じる可能性があります。たとえば、次のプログラムは 100 ポイントの配列を作成して初期化します。Point をクラスとして実装すると、101 個の個別のオブジェクトがインスタンス化されます。1 つは配列用、もう 1 つは 100 要素用です。
class Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class Test
{
static void Main() {
Point[] points = new Point[100];
for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
}
}
別の方法は、Point を構造体にすることです。
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
これで、1 つのオブジェクト (配列用のオブジェクト) のみがインスタンス化され、Point インスタンスが配列内にインラインで格納されます。
構造体コンストラクターは new 演算子で呼び出されますが、これはメモリが割り当てられていることを意味するものではありません。オブジェクトを動的に割り当ててそれへの参照を返す代わりに、構造体コンストラクターは単に構造体値自体を (通常はスタック上の一時的な場所に) 返し、この値は必要に応じてコピーされます。
クラスでは、2 つの変数が同じオブジェクトを参照する可能性があるため、1 つの変数に対する操作が、他の変数によって参照されるオブジェクトに影響を与える可能性があります。構造体では、変数はそれぞれ独自のデータのコピーを持ち、一方の操作が他方に影響を与えることはできません。たとえば、次のコード フラグメントによって生成される出力は、Point がクラスか構造体かによって異なります。
Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);
Point がクラスの場合、a と b は同じオブジェクトを参照するため、出力は 20 になります。Point が構造体の場合、a から b への代入によって値のコピーが作成され、このコピーが ax へのその後の代入の影響を受けないため、出力は 10 になります。
前の例では、構造体の 2 つの制限が強調されています。まず、構造体全体をコピーするのは、通常、オブジェクト参照をコピーするよりも効率が悪いため、代入と値パラメーターの受け渡しは、参照型よりも構造体の方が高くつく可能性があります。第 2 に、ref および out パラメータを除き、構造体への参照を作成することはできません。これにより、多くの状況で構造体の使用が除外されます。
ここに基本的なルールがあります。
すべてのメンバー フィールドが値型の場合は、structを作成します。
いずれかのメンバー フィールドが参照型である場合は、classを作成します。これは、いずれにしても参照型フィールドにヒープ割り当てが必要になるためです。
例
public struct MyPoint
{
public int X; // Value Type
public int Y; // Value Type
}
public class MyPointWithName
{
public int X; // Value Type
public int Y; // Value Type
public string Name; // Reference Type
}
最初: 相互運用シナリオ、またはメモリ レイアウトを指定する必要がある場合
2番目:とにかくデータが参照ポインタとほぼ同じサイズの場合。
ランタイムによって直接使用される値の型と、PInvoke の目的でその他のさまざまな値の型を除いて、2 つのシナリオでのみ値の型を使用する必要があります。
.NET はvalue types
and をサポートしていreference types
ます (Java では、参照型のみを定義できます)。getのインスタンスはreference types
マネージド ヒープに割り当てられ、それらへの未解決の参照がない場合にガベージ コレクションされます。value types
一方、 のインスタンスはに割り当てられるstack
ため、割り当てられたメモリはスコープが終了するとすぐに回収されます。そしてもちろん、value types
値と参照によって渡されますreference types
。System.String を除くすべての C# プリミティブ データ型は値型です。
クラスに対して構造体を使用する場合、
C# では、structs
are value types
、クラスはreference types
. enum
キーワードとキーワードを使用して、C# で値型を作成できますstruct
。a のvalue type
代わりに aを使用するreference type
と、マネージ ヒープ上のオブジェクトが少なくなり、ガベージ コレクター (GC) の負荷が減り、GC サイクルの頻度が減り、結果としてパフォーマンスが向上します。ただし、value types
欠点もあります。bigstruct
を渡すことは、参照を渡すことよりも間違いなくコストがかかります。これは明らかな問題の 1 つです。もう 1 つの問題は、関連するオーバーヘッドboxing/unboxing
です。どういう意味か疑問に思っている場合はboxing/unboxing
、これらのリンクをたどって、boxing
とunboxing
. パフォーマンスとは別に、単に型に値のセマンティクスが必要な場合がありますが、それしかない場合reference types
は実装が非常に困難 (または醜い) になります。value types
コピー セマンティクスが必要な場合、または自動初期化が必要な場合にのみ使用する必要があります。通常arrays
はこれらのタイプで使用します。
C# またはその他の .net 言語の構造体型は、通常、固定サイズの値のグループのように動作する必要があるものを保持するために使用されます。構造体型の便利な側面は、構造体型インスタンスのフィールドは、それが保持されているストレージの場所を変更することによって変更できるということです。それ以外の方法はありません。フィールドを変更する唯一の方法は、まったく新しいインスタンスを構築し、構造体の代入を使用してターゲットのすべてのフィールドを新しいインスタンスの値で上書きすることであるような方法で構造体をコーディングすることは可能ですが、構造体が、そのフィールドにデフォルト以外の値を持つインスタンスを作成する手段を提供しない限り、構造体自体が変更可能な場所に格納されている場合、そのすべてのフィールドは変更可能になります。
構造体にプライベートなクラス型フィールドが含まれていて、それ自体のメンバーをラップされたクラス オブジェクトのメンバーにリダイレクトする場合、基本的にクラス型のように動作するように構造体型を設計できることに注意してください。たとえば、 aPersonCollection
はプロパティSortedByName
およびを提供しSortedById
、どちらも a への「不変の」参照を保持し(コンストラクターで設定)、 または のいずれかを呼び出しPersonCollection
て実装します。このような構造体は、メソッドが. 構造体で配列の一部をラップすることもできます (たとえば、 called 、 int 、およびintを保持する構造体を定義できます)。GetEnumerator
creator.GetNameSortedEnumerator
creator.GetIdSortedEnumerator
PersonCollection
GetEnumerator
PersonCollection
ArrayRange<T>
T[]
Arr
Offset
Length
idx
、 0 から までの範囲のインデックスのLength-1
場合、 にアクセスするインデックス付きプロパティを使用しますArr[idx+Offset]
)。残念ながら、foo
がそのような構造体の読み取り専用インスタンスである場合、現在のコンパイラ バージョンでは のような操作は許可されませんfoo[3]+=4;
。これは、そのような操作が のフィールドに書き込みを試みるかどうかを判断する方法がないためですfoo
。
可変サイズのコレクション (構造体がコピーされるたびにコピーされるように見える) を保持する値型のように動作するように構造体を設計することもできますが、それを機能させる唯一の方法は、構造体は参照を保持し、それを変更する可能性のあるものにさらされます。たとえば、プライベート配列を保持する配列のような構造体を持つことができ、そのインデックス付きの「put」メソッドは、1 つの変更された要素を除いて元の内容と同様の内容を持つ新しい配列を作成します。残念ながら、そのような構造体を効率的に実行するのはやや難しい場合があります。構造体のセマンティクスが便利な場合もありますが (たとえば、配列のようなコレクションをルーチンに渡すことができ、呼び出し元と呼び出し先の両方が、外部コードがコレクションを変更しないことを知っているため、
いや - 私はルールに完全に同意しません. これらは、パフォーマンスと標準化に関して検討するのに適したガイドラインですが、可能性を考慮したものではありません。
回答からわかるように、それらを使用する創造的な方法はたくさんあります。したがって、これらのガイドラインは、常にパフォーマンスと効率のために、それである必要があります。
この場合、クラスを使用して実世界のオブジェクトをより大きな形で表現し、構造体を使用して、より正確な用途を持つ小さなオブジェクトを表現します。あなたが言ったように、「よりまとまりのある全体」。キーワードはまとまり。クラスはよりオブジェクト指向の要素になりますが、構造体は小規模ではありますが、これらの特性の一部を持つことができます。IMO。
一般的な静的属性に非常にすばやくアクセスできる Treeview および Listview タグでそれらを頻繁に使用します。私はいつもこの情報を別の方法で入手するのに苦労してきました。たとえば、私のデータベース アプリケーションでは、テーブル、SP、関数、またはその他のオブジェクトを含む Treeview を使用しています。構造体を作成して入力し、タグに入れ、引き出し、選択範囲のデータを取得します。私はクラスでこれをしません!
私はそれらを小さく保ち、単一のインスタンスの状況で使用し、変更しないようにしています。メモリ、割り当て、およびパフォーマンスを認識することは賢明です。そして、テストはとても必要です。
クラスは参照型です。クラスのオブジェクトが作成されると、オブジェクトが割り当てられる変数は、そのメモリへの参照のみを保持します。オブジェクト参照が新しい変数に割り当てられると、新しい変数は元のオブジェクトを参照します。どちらの変数も同じデータを参照するため、一方の変数で行われた変更は他方の変数に反映されます。構造体は値型です。構造体が作成されると、構造体が割り当てられた変数が構造体の実際のデータを保持します。構造体が新しい変数に割り当てられると、それがコピーされます。したがって、新しい変数と元の変数には、同じデータの 2 つの別個のコピーが含まれます。1 つのコピーに加えられた変更は、他のコピーには影響しません。一般に、クラスは、より複雑な動作や、クラス オブジェクトの作成後に変更する予定のデータをモデル化するために使用されます。
My rule is
1, Always use class;
2, If there is any performance issue, I try to change some class to struct depending on the rules which @IAbstract mentioned, and then do a test to see if these changes can improve performance.
私はちょうど Windows Communication Foundation [WCF] Named Pipe を扱っていましたが、データの交換が参照型ではなく値型であることを保証するために構造体を使用することが理にかなっていることに気付きました。
良い最初の近似は「決して」ではないと思います。
適切な 2 番目の概算は「決して」ではないと思います。
パフォーマンスがどうしても必要な場合は、それらを検討してください。ただし、常に測定してください。
私はめったに物事に構造体を使用しません。しかし、それは私だけです。オブジェクトをnull可能にする必要があるかどうかによって異なります。
他の回答で述べたように、私は実世界のオブジェクトにクラスを使用しています。また、構造体は少量のデータを格納するために使用されるという考え方もあります。
構造は、ほとんどの点でクラス/オブジェクトに似ています。構造体には、関数、メンバーを含めることができ、継承することができます。しかし、構造体はデータ保持のためだけに使用される C#です。構造体は、クラスよりもRAM の使用量が少なく、ガベージ コレクターによる収集が容易です。しかし、構造体で関数を使用すると、コンパイラは実際にその構造体をクラス/オブジェクトと非常によく似たものにするため、関数で何かが必要な場合は、クラス/オブジェクトを使用します。