C# では、クラスではなく構造体を使用する必要があるのはどのような場合でしょうか? 私の概念モデルでは、構造体は、項目が単なる値型のコレクションである場合に使用されます。 それらすべてを論理的にまとめてまとまりのある全体にする方法です。
私はこれらのルールに遭遇しましたここ:
- 構造体は単一の値を表す必要があります。
- 構造体のメモリ フットプリントは 16 バイト未満である必要があります。
- 構造体は作成後に変更しないでください。
これらのルールは機能しますか? 構造体は意味的に何を意味しますか?
ベストアンサー1
OPが参照した情報源にはある程度の信頼性があるが、Microsoftはどうだろうか。構造体の使用に関するスタンスはどうなっているのだろうか。私はさらに情報を探した。マイクロソフトから学ぶそして私が見つけたものは次のとおりです:
型のインスタンスが小さく、通常は短命であるか、または通常は他のオブジェクトに埋め込まれている場合は、クラスではなく構造体を定義することを検討してください。
型が以下の特性をすべて備えていない限り、構造体を定義しないでください。
- プリミティブ型 (整数、倍精度など) と同様に、論理的に単一の値を表します。
- インスタンス サイズが 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
列挙子が要求されるたびにコピーする構造体を使用していることがわかります... 理にかなっています。- Dictionary クラスの内部です。Dictionary
Enumerator
は列挙可能であり、IEnumerator インターフェイスの実装 (IEnumerator ゲッターなど) と同等のアクセス権限を持つ必要があるため、public になります。
更新- さらに、構造体がインターフェースを実装し (Enumerator の場合と同様)、その実装された型にキャストされると、構造体は参照型になり、ヒープに移動されることに注意してください。Dictionary クラスの内部では、Enumerator は依然として値型です。ただし、メソッドが を呼び出すとすぐにGetEnumerator()
、参照型IEnumerator
が返されます。
ここで見られないのは、構造体を不変に保つ、またはインスタンスのサイズを 16 バイト以下に維持するという要件の試みや証明です。
- 上記の構造体には何も宣言されていない
readonly
ため、不変ではありません。 - これらの構造体のサイズは16バイトをはるかに超える可能性があります
Entry
有効期間は未定です(から、、またはガベージコレクションAdd()
まで)。Remove()
Clear()
そして... 4. 両方の構造体は TKey と TValue を格納しますが、これらは参照型として十分機能することが知られています (追加のボーナス情報)
ハッシュ化されたキーにもかかわらず、辞書が高速なのは、構造体のインスタンス化が参照型よりも速いためです。ここでは、Dictionary<int, int>
順番に増分されるキーを持つ 300,000 個のランダムな整数を格納する があります。
容量: 312874
メモリサイズ: 2660827 バイト
サイズ変更完了: 5 ミリ秒
合計所要時間: 889 ミリ秒
容量: 内部配列のサイズを変更する前に使用可能な要素の数。
MemSize : 辞書を MemoryStream にシリアル化し、バイト長を取得することによって決定されます (目的に十分な精度)。
サイズ変更完了: 内部配列を 150862 要素から 312874 要素にサイズ変更するのにかかる時間。各要素が を介して順番にコピーされることを考慮するとArray.CopyTo()
、これは悪くない結果です。
合計充填時間: ログ記録とソースに追加したイベントによって確かに歪んでいますOnResize
が、それでも操作中に 15 回サイズ変更しながら 30 万個の整数を充填するのは印象的です。好奇心からお聞きしますが、容量がすでにわかっている場合、合計充填時間はどのくらいになりますか? 13 ミリ秒
では、Entry
クラスだったらどうなるでしょうか? これらの時間や測定基準は本当にそれほど異なるのでしょうか?
容量: 312874
メモリサイズ: 2660827 バイト
サイズ変更完了: 26 ミリ秒
合計所要時間: 964 ミリ秒
明らかに、大きな違いはサイズ変更にあります。Dictionary が Capacity で初期化された場合、何か違いがありますか? 心配するほどではありません... 12 ミリ秒。
構造体なので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: 構造設計。 要するに:
構造体にデフォルトのコンストラクターを提供しないでください。
構造体に既定のコンストラクターが定義されている場合、構造体の配列が作成されると、共通言語ランタイムは各配列要素に対して既定のコンストラクターを自動的に実行します。
C# コンパイラなどの一部のコンパイラでは、構造体にデフォルト コンストラクターを持たせることはできません。
それは実際には非常に単純なもので、アシモフのロボット工学三原則:
- 構造体は安全に使用できる必要があります
- 構造体はルール#1に違反しない限り、その機能を効率的に実行する必要がある。
- ルール#1を満たすために破壊が必要でない限り、構造体は使用中にそのままの状態を保たなければなりません。
...このことから何を学ぶべきでしょうか。簡単に言うと、値型の使用には責任を持つということです。値型は高速かつ効率的ですが、適切に保守されていない場合は、予期しない動作を引き起こす可能性があります (つまり、意図しないコピー)。