構造体が継承をサポートしないのはなぜですか?


128

.NETの構造体は継承をサポートしないことを知っていますが、なぜこのように制限されるのは明確ではありません。

構造体が他の構造体から継承できないようにする技術的な理由は何ですか?


6
私はこの機能に夢中ではありませんが、構造体の継承が役立ついくつかのケースを考えることができます。Point2D構造体を継承付きのPoint3D構造体に拡張したい場合、Int32から継承して値を制限したい場合があります。 1から100の間で、複数のファイルにわたって表示されるtype-defを作成したい場合があります(typeA = typeBを使用するトリックにはファイルスコープのみがあります)など
Juliet

4
stackoverflow.com/questions/1082311/…を読むとよいでしょう。これは、構造体について少し詳しく説明し、構造体を特定のサイズに制限する必要がある理由を示しています。構造体で継承を使用したい場合は、おそらくクラスを使用する必要があります。
ジャスティン

1
そして、あなたが読みたいかもしれませんstackoverflow.com/questions/1222935/...をそれだけでDOTNETプラットフォームで実行できなかった理由、それは深さに行くように。彼らは冷たくしてC ++のやり方にしましたが、同じ問題がマネージドプラットフォームにとって悲惨なものになる可能性があります。
Dykam 2009

@Justinクラスには、構造体が回避できるパフォーマンスコストがあります。そして、本当に重要なゲーム開発において。したがって、場合によっては、それを助けることができるのであればクラスを使用すべきではありません。
Gavin Williams、

@Dykam C#で実行できると思います。悲惨なことは誇張です。テクニックに慣れていないときに、C#で悲惨なコードを今日書くことができます。だからそれは本当に問題ではありません。構造体の継承がいくつかの問題を解決し、特定のシナリオでパフォーマンスを向上させることができるなら、私はそれですべてです。
Gavin Williams、

回答:


121

値型が継承をサポートできない理由は、配列が原因です。

問題は、パフォーマンスと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を実行すると、問題なくコンパイルおよび実行されます。しかし、BaseDerivedが値型であり、配列が値をインラインで格納する場合、問題が発生します。

について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


11
その問題に対する賢明な解決策は、Base []からDetived []へのキャストを許可しないことです。short []からint []へのキャストが禁止されているように、shortからintへのキャストは可能です。
Niki、

3
+答え:継承の問題は、配列で表現するまで私には問題になりませんでした。別のユーザーは、この問題は構造体を適切なサイズに「スライス」することで軽減できると述べましたが、私はスライスが解決するよりも多くの問題の原因であると考えています。
ジュリエット

4
はい。ただし、配列変換は明示的な変換ではなく暗黙的な変換のためであるため、これは「理にかなっています」。shortからintへの変換は可能ですが、キャストが必要なため、short []をint []に変換できないのが賢明です( 'a.Select(x =>(int)x)。 ) ')。ランタイムがBaseからDerivedへのキャストを許可しない場合、それは「ワート」になります。これは、参照タイプで許可されているためです。したがって、2つの異なる「いぼ」があります。構造体の継承を禁止するか、派生の配列から基本の配列への変換を禁止します。
jonp 08

3
少なくとも構造の継承を防ぐことで、別のキーワードがあり、あるセット(クラス)では機能するものの、別のセット(クラス)では機能しないものに「ランダムな」制限があるのとは対照的に、「構造体は特別」と簡単に言うことができます。 。構造体の制限は説明がはるかに簡単だと思います(「違います!」)。
jonp 08

2
関数の名前を「square」から「double」に変更する必要がある
John

69

構造体がサポートする継承を想像してください。次に宣言:

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?

5
C ++は「スライシング」の概念を導入することでこれに答えたので、それは解決可能な問題です。それでは、構造体の継承がサポートされないのはなぜですか?
jonp 08

1
継承可能な構造体の配列を検討してください。C#は(メモリ)管理言語であることを覚えておいてください。スライスや同様のオプションは、CLRの基本に大混乱をもたらします。
ケナンEK

4
@jonp:はい、解決可能です。望ましい?これが思考実験です:基本クラスVector2D(x、y)と派生クラスVector3D(x、y、z)があると想像してください。両方のクラスには、それぞれsqrt(x ^ 2 + y ^ 2)およびsqrt(x ^ 2 + y ^ 2 + z ^ 2)を計算するMagnitudeプロパティがあります。'Vector3D a = Vector3D(5、10、15);と書くと、Vector2D b = a; '、' a.Magnitude == b.Magnitude 'は何を返す必要がありますか?次に 'a =(Vector3D)b'と書いた場合、a.Magnitudeは割り当て前と割り当て後の値が同じですか?.NET設計者たちはおそらく「いや、それはない」と自分たちに言いました。
ジュリエット

1
問題が解決できるからといって、それが解決されるべきだという意味ではありません。問題が発生する状況を回避することが最善の場合もあります。
Dan Diplo、

@ kek444:構造体持つFoo継承をBar許可してはならないFooに割り当てるのBarタイプの特別な名前のメンバーを作成します(1):しかし、方法が有用な効果のカップルを許すことができると構造体を宣言するBarの最初の項目としてFoo、としているFooが含まのメンバーのエイリアスとなるメンバー名Bar。以前はすべての参照をで置き換える必要なく、代わりにBarを使用するように調整されていたコードを許可し 、のすべてのフィールドをグループとして読み書きする機能を保持します。...Foothing.BarMemberthing.theBar.BarMemberBar
supercat

14

構造体は参照を使用しません(ボックス化されている場合を除いて、使用しないようにする必要があります)。したがって、参照ポインタによる間接参照がないため、ポリモーフィズムは意味がありません。オブジェクトは通常ヒープ上に存在し、参照ポインターを介して参照されますが、構造体はスタック上に割り当てられるか(ボックス化されていない場合)、ヒープ上の参照型が占めるメモリの「内部」に割り当てられます。


8
継承を利用するためにポリモーフィズムを使用する必要はありません
rmeador

では、.NETにはいくつの異なる種類の継承があるでしょうか。
John Saunders、

ポリモーフィズムは構造体に存在します。カスタム構造体に実装する場合と、ToString()のカスタム実装が存在しない場合のToString()の呼び出しの違いを考慮してください。
Kenan EK

これらはすべてSystem.Objectから派生しているためです。これは、構造体よりもSystem.Object型の多態性です。
ジョンサンダース、

ポリモーフィズムは、ジェネリック型パラメーターとして使用される構造で意味を持つ場合があります。ポリモーフィズムは、インターフェースを実装する構造体で機能します。インターフェースの最大の問題は、構造体フィールドにbyrefを公開できないことです。そうでなければ、私が思う最大のものは、これまでのように参考になる構造体を「継承する」タイプ(構造体やクラス)を有するの手段となりFoo構造型のフィールドがありBarについてのことができるようにBarなるように、独自のなどのメンバーをPoint3dクラスは、例えば、カプセル封入AでしPoint2d xyたがを参照してください。Xどちらかとして、そのフィールドのxy.XX
スーパーキャット2013年

8

ドキュメントの内容次のとおりです。

構造体は、値のセマンティクスを持つ小さなデータ構造に特に役立ちます。複素数、座標系のポイント、またはディクショナリのキーと値のペアはすべて、構造体の良い例です。これらのデータ構造の重要な点は、データメンバーがほとんどないこと、継承や参照IDを使用する必要がないこと、そして割り当てが参照の代わりに値をコピーする値のセマンティクスを使用して便利に実装できることです。

基本的に、それらは単純なデータを保持することになっているため、継承などの「追加機能」はありません。おそらく、技術的には制限された種類の継承をサポートすることは可能ですが(スタックにあるため、ポリモーフィズムではありません)、継承をサポートしないことも設計上の選択であると考えています(.NETの他の多くのものと同様)言語は。)

一方、私は継承の利点に同意し、私たちは皆、私たちstructが別のものから継承してもらいたいところに到達し、それが不可能であることを理解していると思います。しかし、その時点では、データ構造はおそらく非常に高度であるため、とにかくクラスでなければなりません。


4
それが相続がない理由ではありません。
Dykam 2009

ここで説明されている継承は、一方が他方から継承して継承する2つの構造体を使用できず、1つの構造体の実装に再利用して追加する(つまり、Point3Dからを作成するPoint2Dことはできません。使用するPoint3D代わりにしPoint2Dていますが、再実装する必要はないだろうPoint3D...私はとにかくそれを解釈する方法ですそれ)ゼロから完全に。
Blixt

つまり、多態性のない継承をサポートできます。そうではありません。私はそれは、人が選択するための設計上の選択だと考えているclass以上struct、適切な場合。
Blixt 2009

3

構造体はスタックに直接配置されるため、継承のようなクラスは不可能です。継承する構造体は親よりも大きくなりますが、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に割り当てられます。


2
C ++はこのケースを問題なく処理します、IIRC。Bのインスタンスは、Aのサイズに合うようにスライスされます。それが.NET構造体のように純粋なデータ型である場合、問題は起こりません。Aを返すメソッドで少し問題が発生し、その戻り値をBに格納していますが、これは許可されません。要するに、.NET設計者は、必要に応じてこれに対処できたはずですが、何らかの理由で対処できませんでした。
rmeador 2009

1
DoSomething()の場合、(C ++のセマンティクスを前提として) 'b'がAインスタンスを作成するために「スライス」されるため、問題が発生する可能性はほとんどありません。問題は<i>配列</ i>にあります。既存のAおよびB構造体と、<c> DoSomething(A []​​ arg){arg [1] .property = 1;} </ c>メソッドを検討してください。値型の配列は値を「インライン」で格納するため、DoSomething(actual = new B [2] {})は、actual [1] .propertyではなく、actual [0] .childpropertyを設定します。これは悪いです。
jonp 08

2
@ジョン:私はそれを主張していなかったし、@ジョンプもそうではなかったと思う。この問題が古く、解決されたことを単に述べただけなので、.NET設計者は、技術的な実行不可能性以外の理由でそれをサポートしないことを選択しました。
rmeador 2009

「派生型の配列」の問題はC ++にとって新しいものではないことに注意してください。parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4を参照してください (C ++の配列は悪いです;-)
jonp

@John:「派生型の配列と基本型が混在しない」問題の解決策は、いつものように、それを行わないことです。これが、C ++の配列が邪魔である(メモリ破損をより簡単に許可する)理由であり、.NETが値型の継承をサポートしていない理由です(コンパイラとJITは、それが起こらないことを保証します)。
jonp 2009

3

訂正したい点があります。構造体を継承できない理由は、それらがスタック上に存在することが正しいものであるためですが、それは同じ半分正しい説明です。構造体は、他の値型と同様に、スタックに置くことができます。それは変数が宣言されている場所に依存するため、スタックまたはヒープのいずれかに存在します。これは、それらがそれぞれローカル変数またはインスタンスフィールドの場合です。

それを言って、セシルは名前を正しく釘付けにしました。

私はこれを強調したいのですが、値型はスタックに存在することができます。これは、彼らがいつもそうしているという意味ではありません。メソッドパラメータを含むローカル変数は、そうなります。他のすべてはしません。それでも、継承できない理由は依然として残っています。:-)


3

構造体はスタックに割り当てられます。つまり、値のセマンティクスはほとんど無料であり、構造体メンバーへのアクセスは非常に安価です。これはポリモーフィズムを妨げません。

各構造体は、その仮想関数テーブルへのポインタで開始することができます。これはパフォーマンスの問題になります(すべての構造体は少なくともポインタのサイズになります)が、実行可能です。これは仮想機能を許可します。

フィールドの追加についてはどうですか?

まあ、スタックに構造体を割り当てるときは、一定量のスペースを割り当てます。必要なスペースは、コンパイル時に(事前に、またはJITするときに)決定されます。フィールドを追加してから基本タイプに割り当てる場合:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

これにより、スタックの不明な部分が上書きされます。

代替策は、ランタイムがsizeof(A)バイトを任意のA変数に書き込むだけでこれを防止することです。

BがAのメソッドをオーバーライドし、そのInteger2フィールドを参照するとどうなりますか?ランタイムがMemberAccessExceptionをスローするか、メソッドが代わりにスタック上のランダムデータにアクセスします。これらはどちらも許可されていません。

構造体を多態的に使用しない限り、または継承時にフィールドを追加しない限り、構造体の継承は完全に安全です。しかし、これらはそれほど有用ではありません。


1
ほとんど。スタックを参照してスライスの問題について言及した人は他にいません。配列についてのみです。そして、他の誰も利用可能な解決策に言及しませんでした。
user38001 2009

1
.netのすべての値タイプは、そのタイプやそれらに含まれるフィールドに関係なく、クレスティオンでゼロで埋められます。構造体にvtableポインタのようなものを追加するには、ゼロ以外のデフォルト値で型を初期化する手段が必要になります。そのような機能はさまざまな目的に役立つ可能性があり、ほとんどの場合にそのような機能を実装することはそれほど難しくはないかもしれませんが、.netに近いものは存在しません。
スーパーキャット2012年

@ user38001 "構造体はスタックに割り当てられます"-それらがインスタンスフィールドでない場合は、ヒープに割り当てられます。
David Klempfner 2017年

2

これは非常に頻繁な質問のようです。値の型は、変数を宣言する「所定の場所」に格納されることを付け加えたいと思います。別に実装の詳細から、存在しないことを、この手段なしオブジェクトについて何かを言うオブジェクトヘッダは、唯一の変数は、種類のデータが存在することを知っています。


コンパイラはそこに何があるかを知っています。C ++を参照することは、答えにはなりません。
Henk Holterman、

どこからC ++を推測しましたか?MSDNのブログ記事を引用するために、スタックは実装の詳細であり、動作に最もよく一致するので、インプレースで言い続けます。
セシルの名前は

はい、C ++について言及するのはよくありませんでした。しかし、ランタイム情報が必要かどうかという質問は別として、構造体に「オブジェクトヘッダー」を含めるべきではないのはなぜですか?コンパイラは好きなようにそれらをマッシュアップできます。[Structlayout]構造体のヘッダーを非表示にすることもできます。
Henk Holterman、

構造体は値型であるため、ランタイムは常に他の値型(制約)の場合と同様にコンテンツをコピーするため、オブジェクトヘッダーでは必要ありません。何という参照型クラスがためのものですので、それは、ヘッダと意味がありません:P
セシルは、名前持っ


0

ILはスタックベースの言語であるため、引数を指定してメソッドを呼び出すと、次のようになります。

  1. 引数をスタックにプッシュします
  2. メソッドを呼び出します。

メソッドが実行されると、スタックからいくつかのバイトをポップして引数を取得します。それは知っている正確引数が参照型ポインタ(32ビットで常に4バイト)のいずれかであるか、サイズが常に正確に知られている値型であるため、ポップオフするバイト数。

参照型ポインターの場合、メソッドはヒープ内のオブジェクトを検索し、その型ハンドルを取得します。これは、その正確な型の特定のメソッドを処理するメソッドテーブルを指します。値タイプの場合、値タイプは継承をサポートしていないため、メソッドテーブルへのルックアップは必要ありません。したがって、可能なメソッド/タイプの組み合わせは1つだけです。

値の型が継承をサポートしている場合、構造体の特定の型とその値をスタックに配置する必要があるという余分なオーバーヘッドが発生します。これは、型の特定の具象インスタンスのメソッドテーブル検索の一種を意味します。これにより、値タイプの速度と効率の利点がなくなります。


1
C ++は、本当の問題のために、この答えを読むことを解決した:stackoverflow.com/questions/1222935/...
Dykam
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.