.NETの構造体は継承をサポートしないことを知っていますが、なぜこのように制限されるのかは明確ではありません。
構造体が他の構造体から継承できないようにする技術的な理由は何ですか?
.NETの構造体は継承をサポートしないことを知っていますが、なぜこのように制限されるのかは明確ではありません。
構造体が他の構造体から継承できないようにする技術的な理由は何ですか?
回答:
値型が継承をサポートできない理由は、配列が原因です。
問題は、パフォーマンスとGCの理由で、値型の配列が「インライン」で格納されることです。たとえば、が参照型のnew FooType[10] {...}
場合FooType
、マネージヒープに11個のオブジェクトが作成されます(配列に1個、型インスタンスごとに10個)。FooType
が値型の場合は、マネージヒープ上に1つのインスタンスのみが作成されます(配列自体の場合、各配列値は配列と「インライン」で格納されるため)。
ここで、値型の継承があるとします。上記の配列の「インラインストレージ」動作と組み合わせると、C ++に見られるように、悪いことが起こります。
次の疑似C#コードを考えてみます。
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
通常の変換ルールでは、a Derived[]
はaに変換できますBase[]
(良いか悪いかを問わず)。したがって、上記の例でs / struct / class / gを実行すると、問題なくコンパイルおよび実行されます。しかし、Base
とDerived
が値型であり、配列が値をインラインで格納する場合、問題が発生します。
についてSquare()
何も知らないため、問題がありDerived
ます。それは、ポインター算術のみを使用して、配列の各要素にアクセスし、一定量(sizeof(A)
)ずつ増加します。アセンブリは漠然と次のようになります。
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(はい、それはひどいアセンブリですが、要点は、派生型が使用されていることを知らずに、既知のコンパイル時定数で配列をインクリメントすることです。)
したがって、これが実際に発生した場合、メモリ破損の問題が発生します。具体的には、内Square()
、values[1].A*=2
でしょう実際に変更することvalues[0].B
!
デバッグしようTHAT!
構造体がサポートする継承を想像してください。次に宣言:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
構造体変数のサイズが固定されていないことを意味し、それが参照型を持っている理由です。
さらに良いことに、これを考慮してください:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
継承をBar
許可してはならないFoo
に割り当てるのBar
タイプの特別な名前のメンバーを作成します(1):しかし、方法が有用な効果のカップルを許すことができると構造体を宣言するBar
の最初の項目としてFoo
、としているFoo
が含まのメンバーのエイリアスとなるメンバー名Bar
。以前はすべての参照をで置き換える必要なく、代わりにBar
を使用するように調整されていたコードを許可し 、のすべてのフィールドをグループとして読み書きする機能を保持します。...Foo
thing.BarMember
thing.theBar.BarMember
Bar
構造体は参照を使用しません(ボックス化されている場合を除いて、使用しないようにする必要があります)。したがって、参照ポインタによる間接参照がないため、ポリモーフィズムは意味がありません。オブジェクトは通常ヒープ上に存在し、参照ポインターを介して参照されますが、構造体はスタック上に割り当てられるか(ボックス化されていない場合)、ヒープ上の参照型が占めるメモリの「内部」に割り当てられます。
Foo
構造型のフィールドがありBar
についてのことができるようにBar
なるように、独自のなどのメンバーをPoint3d
クラスは、例えば、カプセル封入AでしPoint2d xy
たがを参照してください。X
どちらかとして、そのフィールドのxy.X
かX
。
構造体は、値のセマンティクスを持つ小さなデータ構造に特に役立ちます。複素数、座標系のポイント、またはディクショナリのキーと値のペアはすべて、構造体の良い例です。これらのデータ構造の重要な点は、データメンバーがほとんどないこと、継承や参照IDを使用する必要がないこと、そして割り当てが参照の代わりに値をコピーする値のセマンティクスを使用して便利に実装できることです。
基本的に、それらは単純なデータを保持することになっているため、継承などの「追加機能」はありません。おそらく、技術的には制限された種類の継承をサポートすることは可能ですが(スタックにあるため、ポリモーフィズムではありません)、継承をサポートしないことも設計上の選択であると考えています(.NETの他の多くのものと同様)言語は。)
一方、私は継承の利点に同意し、私たちは皆、私たちstruct
が別のものから継承してもらいたいところに到達し、それが不可能であることを理解していると思います。しかし、その時点では、データ構造はおそらく非常に高度であるため、とにかくクラスでなければなりません。
Point3D
からを作成するPoint2D
ことはできません。使用するPoint3D
代わりにしPoint2D
ていますが、再実装する必要はないだろうPoint3D
...私はとにかくそれを解釈する方法ですそれ)ゼロから完全に。
class
以上struct
、適切な場合。
構造体はスタックに直接配置されるため、継承のようなクラスは不可能です。継承する構造体は親よりも大きくなりますが、JITはそれを認識せず、あまりにも少ないスペースに多くを配置しようとします。少し不明瞭に聞こえますが、例を書いてみましょう:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
これが可能であれば、次のスニペットでクラッシュします。
void DoSomething(A arg){};
...
B b;
DoSomething(b);
スペースは、sizeof Bではなく、sizeof Aに割り当てられます。
訂正したい点があります。構造体を継承できない理由は、それらがスタック上に存在することが正しいものであるためですが、それは同じ半分正しい説明です。構造体は、他の値型と同様に、スタックに置くことができます。それは変数が宣言されている場所に依存するため、スタックまたはヒープのいずれかに存在します。これは、それらがそれぞれローカル変数またはインスタンスフィールドの場合です。
それを言って、セシルは名前を正しく釘付けにしました。
私はこれを強調したいのですが、値型はスタックに存在することができます。これは、彼らがいつもそうしているという意味ではありません。メソッドパラメータを含むローカル変数は、そうなります。他のすべてはしません。それでも、継承できない理由は依然として残っています。:-)
構造体はスタックに割り当てられます。つまり、値のセマンティクスはほとんど無料であり、構造体メンバーへのアクセスは非常に安価です。これはポリモーフィズムを妨げません。
各構造体は、その仮想関数テーブルへのポインタで開始することができます。これはパフォーマンスの問題になります(すべての構造体は少なくともポインタのサイズになります)が、実行可能です。これは仮想機能を許可します。
フィールドの追加についてはどうですか?
まあ、スタックに構造体を割り当てるときは、一定量のスペースを割り当てます。必要なスペースは、コンパイル時に(事前に、またはJITするときに)決定されます。フィールドを追加してから基本タイプに割り当てる場合:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
これにより、スタックの不明な部分が上書きされます。
代替策は、ランタイムがsizeof(A)バイトを任意のA変数に書き込むだけでこれを防止することです。
BがAのメソッドをオーバーライドし、そのInteger2フィールドを参照するとどうなりますか?ランタイムがMemberAccessExceptionをスローするか、メソッドが代わりにスタック上のランダムデータにアクセスします。これらはどちらも許可されていません。
構造体を多態的に使用しない限り、または継承時にフィールドを追加しない限り、構造体の継承は完全に安全です。しかし、これらはそれほど有用ではありません。
これは非常に頻繁な質問のようです。値の型は、変数を宣言する「所定の場所」に格納されることを付け加えたいと思います。別に実装の詳細から、存在しないことを、この手段なしオブジェクトについて何かを言うオブジェクトヘッダは、唯一の変数は、種類のデータが存在することを知っています。
構造体はインターフェースをサポートしているので、ポリモーフィックなことをそのように行うことができます。
ILはスタックベースの言語であるため、引数を指定してメソッドを呼び出すと、次のようになります。
メソッドが実行されると、スタックからいくつかのバイトをポップして引数を取得します。それは知っている正確引数が参照型ポインタ(32ビットで常に4バイト)のいずれかであるか、サイズが常に正確に知られている値型であるため、ポップオフするバイト数。
参照型ポインターの場合、メソッドはヒープ内のオブジェクトを検索し、その型ハンドルを取得します。これは、その正確な型の特定のメソッドを処理するメソッドテーブルを指します。値タイプの場合、値タイプは継承をサポートしていないため、メソッドテーブルへのルックアップは必要ありません。したがって、可能なメソッド/タイプの組み合わせは1つだけです。
値の型が継承をサポートしている場合、構造体の特定の型とその値をスタックに配置する必要があるという余分なオーバーヘッドが発生します。これは、型の特定の具象インスタンスのメソッドテーブル検索の一種を意味します。これにより、値タイプの速度と効率の利点がなくなります。