C#またはJavaの次のバージョンに多重継承を含めるかどうかを常に尋ねる人がいます。この能力を持つ幸運なC ++の人々は、これは誰かが最終的に自分自身を吊るすためのロープを与えるようなものだと言います。
多重継承の問題は何ですか?具体的なサンプルはありますか?
C#またはJavaの次のバージョンに多重継承を含めるかどうかを常に尋ねる人がいます。この能力を持つ幸運なC ++の人々は、これは誰かが最終的に自分自身を吊るすためのロープを与えるようなものだと言います。
多重継承の問題は何ですか?具体的なサンプルはありますか?
回答:
最も明らかな問題は、関数のオーバーライドにあります。
2つのクラスA
とがありB
、どちらもメソッドを定義しているとしますdoSomething
。次にC
、A
およびの両方から継承する3番目のクラスを定義しB
ますが、doSomething
メソッドをオーバーライドしません。
コンパイラがこのコードをシードするとき...
C c = new C();
c.doSomething();
...メソッドのどの実装で使用する必要がありますか?これ以上の説明がないと、コンパイラーが曖昧さを解決することは不可能です。
オーバーライドの他に、多重継承のもう1つの大きな問題は、メモリ内の物理オブジェクトのレイアウトです。
C ++やJava、C#などの言語は、オブジェクトのタイプごとに固定アドレスベースのレイアウトを作成します。このようなもの:
class A:
at offset 0 ... "abc" ... 4 byte int field
at offset 4 ... "xyz" ... 8 byte double field
at offset 12 ... "speak" ... 4 byte function pointer
class B:
at offset 0 ... "foo" ... 2 byte short field
at offset 2 ... 2 bytes of alignment padding
at offset 4 ... "bar" ... 4 byte array pointer
at offset 8 ... "baz" ... 4 byte function pointer
コンパイラは、マシンコード(またはバイトコード)を生成するときに、それらの数値オフセットを使用して各メソッドまたはフィールドにアクセスします。
多重継承は非常にトリッキーになります。
クラスC
がとの両方A
を継承する場合B
、コンパイラはデータをAB
順番にレイアウトするか、順番にレイアウトするかを決定する必要がありますBA
。
しかし、B
オブジェクトのメソッドを呼び出しているとしましょう。本当にB
?それともC
、そのB
インターフェイスを介して実際に多態的に呼び出されているオブジェクトですか?オブジェクトの実際のIDに応じて、物理的なレイアウトは異なり、呼び出しサイトで呼び出す関数のオフセットを知ることは不可能です。
この種のシステムを処理する方法は、固定レイアウトアプローチを廃止して、関数の呼び出しやフィールドへのアクセスを試行する前に、各オブジェクトのレイアウトを照会できるようにすることです。
ですから...簡単に言えば、コンパイラの作成者が多重継承をサポートするのは大変です。したがって、Guido van Rossumのような人がpythonを設計するとき、またはAnders Hejlsbergがc#を設計するとき、多重継承をサポートすることでコンパイラーの実装が大幅に複雑になることを知っており、おそらくそのメリットはコストに見合うものではないと考えています。
皆さんが言及する問題は、実際にはそれほど難しいものではありません。実際、例えばエッフェルはそれを完璧にこなします!(そして任意の選択などを導入することなく)
たとえば、AとBから継承し、両方にメソッドfoo()がある場合、もちろん、クラスCでAとBの両方から継承する任意の選択は必要ありません。 c.foo()が呼び出された場合、またはCのメソッドの1つを名前変更する必要がある場合に使用されます(それはbar()になる可能性があります)
また、多重継承はしばしば非常に役立つと思います。エッフェルのライブラリーを見ると、至る所で使用されていることがわかります。個人的には、Javaでのプログラミングに戻らなければならなかったときに、その機能を逃してしまいました。
2つのクラスBとCがAから継承し、クラスDがBとCの両方から継承する場合に生じるあいまいさ。BとCがオーバーライドしたメソッドがあり、Dがオーバーライドしていない場合、メソッドはDを継承します:Bのそれ、またはCのそれ?
...この状況でのクラス継承図の形から、「ダイヤモンド問題」と呼ばれています。この場合、クラスAが一番上にあり、BとCの両方がその下に別々にあり、Dが2つを一番下で結合して菱形を形成しています...
someZ
キャストしたい場合はどうObject
なりB
ますか?それB
はどちらになりますか?
Object
とその型へのキャストが返されるという事実に依存しています...
多重継承は、あまり使用されず、誤用される可能性がありますが、必要になる場合があることの1つです。
機能が追加されていないことを理解できなかったのは、それが悪用される可能性があるという理由だけで、良い代替手段がない場合です。インターフェースは多重継承の代替手段ではありません。1つには、事前条件または事後条件を強制することはできません。他のツールと同じように、いつ使用するのが適切で、どのように使用するかを知る必要があります。
assert
?
Cに継承されているオブジェクトAとBがあるとします。AとBはどちらもfoo()を実装し、Cは実装していません。C.foo()を呼び出します。どの実装が選択されますか?他にも問題はありますが、この種のものは大きな問題です。
多重継承の主な問題は、tloachの例とうまくまとめられています。同じ関数またはフィールドを実装する複数の基本クラスから継承する場合、コンパイラーはどの実装を継承するかを決定する必要があります。
同じ基本クラスから継承する複数のクラスから継承する場合、これはさらに悪化します。(ダイヤモンドの継承、継承ツリーを描くとダイヤモンドの形になります)
これらの問題は、コンパイラが克服するために実際には問題ではありません。ただし、コンパイラがここで行う必要のある選択はかなり恣意的であり、これによりコードがはるかに直観的になりません。
優れたOO設計を行う場合、多重継承は必要ありません。必要な場合は、通常、継承を使用して機能を再利用してきましたが、継承は「is-a」関係にのみ適しています。
同じ問題を解決し、多重継承のような問題がないミックスインのような他のテクニックがあります。
([..bool..]? "test": 1)
何ですか?
ダイヤモンドの問題は問題ではないと私は思います。
私の観点から見ると、多重継承の最悪の問題はRADです。被害者と開発者であると主張しているが、実際には知識が(せいぜい)半分しか残っていない人々です。
個人的には、このようなWindowsフォームで最終的に何かを実行できたら非常に嬉しく思います(これは正しいコードではありませんが、アイデアがわかるはずです)。
public sealed class CustomerEditView : Form, MVCView<Customer>
これは、多重継承がない場合の主な問題です。インターフェイスでも同様のことができますが、私が「s ***コード」と呼んでいるものがあります。これは、たとえば、データコンテキストを取得するためにクラスのそれぞれに記述しなければならない、この痛みを伴う反復的なc ***です。
私の意見では、現代の言語でコードを繰り返す必要はまったくなく、最低でもないはずです。
多重継承自体に問題はありません。問題は、最初から多重継承を考慮して設計されていない言語に多重継承を追加することです。
エッフェル言語は非常に効率的かつ生産的な方法で制限なしに多重継承をサポートしていますが、言語はそれをサポートするために最初から設計されました。
この機能はコンパイラ開発者にとって実装が複雑ですが、優れた多重継承サポートが他の機能のサポートを回避できる(つまり、インターフェイスや拡張メソッドの必要がない)ことで欠点を補うことができるようです。
多重継承をサポートするかどうかは、選択の問題、優先順位の問題だと思います。機能が複雑になるほど、正しく実装されて動作するまでに時間がかかり、問題が生じる可能性があります。C ++実装が多重継承がC#およびJavaで実装されなかった理由かもしれません...
Javaや.NETなどのフレームワークの設計目標の1つは、コンパイルされたコードが、コンパイル済みライブラリの1つのバージョンで機能することを可能にすることです。新しい機能を追加します。CやC ++などの言語の通常のパラダイムは、必要なすべてのライブラリを含む静的にリンクされた実行可能ファイルを配布することですが、.NETおよびJavaのパラダイムは、実行時に「リンク」されるコンポーネントのコレクションとしてアプリケーションを配布することです。
.NETに先行するCOMモデルはこの一般的なアプローチを使用しようとしましたが、実際には継承がありませんでした。代わりに、各クラス定義は、すべてのパブリックメンバーを含む同じ名前のクラスとインターフェイスの両方を効果的に定義しました。インスタンスはクラス型でしたが、参照はインターフェース型でした。クラスを別のクラスから派生するものとして宣言することは、クラスを他のクラスのインターフェースを実装するものとして宣言することと同等であり、新しいクラスに、派生元のクラスのすべてのパブリックメンバーを再実装することを要求しました。YとZがXから派生し、WがYとZから派生する場合、Zは実装を使用できないため、YとZがXのメンバーを異なる方法で実装しても問題はありません。自分の。WはYやZのインスタンスをカプセル化します。
Javaと.NETの問題は、コードがメンバーを継承し、それらにアクセスして暗黙的に親メンバーを参照できることです。上記のように関連するクラスWZがあるとします。
class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z // Not actually permitted in C#
{
public static void Test()
{
var it = new W();
it.Foo();
}
}
W.Test()
Wのインスタンスを作成すると、でFoo
定義された仮想メソッドの実装が呼び出されるようになりますX
。ただし、YとZは実際には別々にコンパイルされたモジュールにあり、XとWがコンパイルされたときに上記のように定義されていたが、後で変更されて再コンパイルされたとします。
class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }
では、呼び出しの効果はW.Test()
何でしょうか?配布前にプログラムを静的にリンクする必要がある場合、静的リンクステージは、YとZが変更される前にプログラムに曖昧さはなかったものの、YとZへの変更により不明確になり、リンカーが拒否できることを認識できる場合があります。このようなあいまいさが解消されるまで、または解消されるまで、プログラムをビルドします。一方、WとYおよびZの新しいバージョンの両方を持っている人は、単にプログラムを実行したいだけで、そのいずれのソースコードも持っていない人である可能性があります。ときにW.Test()
実行されると、それはもはやクリアされないであろうものをW.Test()
する必要がありますが、ユーザーが新しいバージョンのYとZでWを実行しようとするまで、システムのどの部分でも問題があると認識できません(WがYとZの変更前であっても不正と見なされた場合を除く) 。
C ++の仮想継承のようなものを使用しない限り、ひし形は問題になりません。通常の継承では、各基本クラスはメンバーフィールドに似ており(実際には、このようにRAMに配置されます)、構文上の砂糖とより多くの仮想メソッドをオーバーライドする追加機能。コンパイル時にあいまいさが生じる可能性がありますが、通常は簡単に解決できます。
一方、仮想継承を使用すると、簡単に制御できなくなります(その後、混乱します)。例として「ハート」図を考えてみましょう:
A A
/ \ / \
B C D E
\ / \ /
F G
\ /
H
C ++では完全に不可能です。単一のクラスにマージされるF
とすぐにG
、それらA
のsもマージされます。あなたは++基底クラスがCに不透明考慮しないかもしれないことを意味は、(この例では、あなたが構築しなければならないA
でH
、あなたが知っている必要がありますので、そのこと階層に存在するどこか)。ただし、他の言語では機能する場合があります。例えば、F
とG
明示的従って結果としてマージを禁止し、効果自体は固体行う「内部」としてAを宣言することができました。
別の興味深い例(C ++固有ではない):
A
/ \
B B
| |
C D
\ /
E
ここでB
は、仮想継承のみを使用します。だから、E
2含まれているB
のは同じことを共有A
。このようにして、をA*
指すポインターを取得E
できB*
ますが、オブジェクトが実際B
にそのようなキャストであることは不明ですが、ポインターにキャストすることはできず、このあいまいさはコンパイル時に検出できません(コンパイラーがプログラム全体)。ここにテストコードがあります:
struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};
int main() {
E data;
E *e = &data;
A *a = dynamic_cast<A *>(e); // works, A is unambiguous
// B *b = dynamic_cast<B *>(e); // doesn't compile
B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
std::cout << "E: " << e << std::endl;
std::cout << "A: " << a << std::endl;
std::cout << "B: " << b << std::endl;
// the next casts work
std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
return 0;
}
さらに、実装は非常に複雑になる場合があります(言語によって異なります。ベンジスミスの回答を参照してください)。