多重継承を C# または Java の次のバージョンに含める必要があるかどうかを常に尋ねる人を見かけます。幸運にもこの能力を持っている C++ の人々は、これは誰かに最終的に首を吊るすためのロープを与えるようなものだと言います。
多重継承の問題は何ですか?具体的なサンプルはありますか?
多重継承を C# または Java の次のバージョンに含める必要があるかどうかを常に尋ねる人を見かけます。幸運にもこの能力を持っている C++ の人々は、これは誰かに最終的に首を吊るすためのロープを与えるようなものだと言います。
多重継承の問題は何ですか?具体的なサンプルはありますか?
最も明白な問題は、関数のオーバーライドにあります。
2 つのクラスA
とがありB
、どちらもメソッド を定義しているとしますdoSomething
。ここで、とのC
両方から継承する 3 番目のクラス を定義しますが、メソッドはオーバーライドしません。A
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
コンパイラがマシン コード (またはバイトコード) を生成するとき、これらの数値オフセットを使用して各メソッドまたはフィールドにアクセスします。
多重継承は非常に扱いにくいものです。
classがとのC
両方から継承する場合、コンパイラはデータを順番にレイアウトするか、順番にレイアウトするかを決定する必要があります。A
B
AB
BA
しかしここで、B
オブジェクトのメソッドを呼び出していると想像してください。それは本当にただB
ですか?それとも、実際には、そのインターフェイスC
を介して多態的に呼び出されるオブジェクトですか? B
オブジェクトの実際のアイデンティティに応じて、物理的なレイアウトは異なり、呼び出しサイトで呼び出す関数のオフセットを知ることは不可能です。
この種のシステムを処理する方法は、固定レイアウト アプローチを捨てて、関数を呼び出したり、そのフィールドにアクセスしたりする前に、各オブジェクトのレイアウトを照会できるようにすることです。
つまり...簡単に言えば...コンパイラの作成者が多重継承をサポートすることは首の痛みです。そのため、Guido van Rossum のような人が Python を設計したり、Anders Hejlsberg が c# を設計したりすると、多重継承をサポートするとコンパイラの実装が大幅に複雑になることを知っており、おそらくコストに見合うメリットがあるとは考えていません。
あなたが言及している問題は、実際には解決するのがそれほど難しくありません。実際、例えばエッフェルはそれを完璧にうまくやっています!(そして、恣意的な選択などを導入することなく)
たとえば、A と B から継承し、どちらもメソッド foo() を持っている場合、もちろん、クラス C で A と B の両方から継承する任意の選択は必要ありません。 c.foo() が呼び出された場合、または C のいずれかのメソッドの名前を変更する必要がある場合に使用されます (bar() になる可能性があります)。
また、多重継承は非常に便利な場合が多いと思います。Eiffel のライブラリーを見ると、Eiffel がいたるところで使用されていることがわかります。個人的には、Java でのプログラミングに戻らなければならなかったときに、この機能を見逃していました。
2 つのクラス B と C が A から継承し、クラス D が B と C の両方から継承する場合に生じるあいまいさ。A に B と C がオーバーライドしたメソッドがあり、D がそれをオーバーライドしない場合、どのバージョンのメソッド D は、B のメソッドを継承しますか?それとも C のメソッドを継承しますか?
…このような場合のクラス継承図の形から「ダイヤモンド問題」と呼ばれています。この場合、クラス A が一番上にあり、B と C の両方がその下にあり、D は 2 つを下に結合して菱形を形成しています...
多重継承はあまり使用されず、誤用される可能性がありますが、必要になる場合もあります。
適切な代替手段がない場合、誤用される可能性があるという理由だけで、機能を追加しないことを理解できませんでした。インターフェイスは、多重継承に代わるものではありません。1 つには、前提条件または事後条件を強制することはできません。他のツールと同様に、いつ、どのように使用するのが適切かを知る必要があります。
オブジェクト A と B があり、どちらも C に継承されているとします。A と B はどちらも foo() を実装していますが、C は実装していません。C.foo() を呼び出します。どの実装が選択されますか? 他にも問題はありますが、この種のことは大きな問題です。
ダイヤモンドの問題は問題ではないと思います。それは詭弁だと思います。
私の観点からすると、多重継承の最悪の問題は RAD です。被害者や、開発者であると主張しているが、実際には半分の知識 (せいぜい) で立ち往生している人々です。
個人的には、最終的に Windows フォームで次のようなことができたらとてもうれしいです (これは正しいコードではありませんが、アイデアが得られるはずです)。
public sealed class CustomerEditView : Form, MVCView<Customer>
これは、多重継承がない場合の主な問題です。インターフェイスでも同様のことができますが、私が「s*** コード」と呼んでいるものがあります。たとえば、データ コンテキストを取得するために各クラスに記述しなければならない、この苦痛な反復 c*** です。
私の意見では、現代の言語でコードを繰り返す必要はまったくないはずです。
多重継承の主な問題は、tloach の例でうまくまとめられています。同じ関数またはフィールドを実装する複数の基本クラスから継承する場合、コンパイラは継承する実装を決定する必要があります。
同じ基本クラスから継承する複数のクラスから継承すると、これはさらに悪化します。(ひし形の継承、継承ツリーを描画するとひし形になります)
これらの問題は、コンパイラが克服するのに実際には問題ではありません。しかし、コンパイラがここで行わなければならない選択はかなり恣意的なものであり、これによりコードははるかに直感的ではなくなります。
優れた OO 設計を行っている場合、多重継承は必要ありません。必要な場合は、通常、継承を使用して機能を再利用してきましたが、継承は「is-a」関係にのみ適しています。
同じ問題を解決し、多重継承が持つ問題を持たないミックスインのような他の手法があります。
Common Lisp Object System(CLOS)は、C ++スタイルの問題を回避しながらMIをサポートするものの別の例です。継承には適切なデフォルトが与えられますが、スーパーの動作を正確に呼び出す方法を明示的に決定する自由があります。 。
多重継承自体には何の問題もありません。問題は、最初から多重継承を考慮して設計されていない言語に多重継承を追加することです。
Eiffel 言語は、制限のない多重継承を非常に効率的かつ生産的な方法でサポートしていますが、言語は最初からそれをサポートするように設計されています。
この機能はコンパイラ開発者にとって実装が複雑ですが、適切な多重継承のサポートにより他の機能のサポートを回避できる (つまり、インターフェイスまたは拡張メソッドが不要) という事実によって、その欠点を補うことができるようです。
多重継承をサポートするかどうかは、選択の問題であり、優先順位の問題だと思います。より複雑な機能は、正しく実装されて動作するまでにより多くの時間がかかり、より物議を醸す可能性があります. C++ の実装が、C# と Java で多重継承が実装されなかった理由かもしれません...
Java や .NET などのフレームワークの設計目標の 1 つは、コンパイル済みのライブラリの 1 つのバージョンで動作するようにコンパイルされたコードが、そのライブラリの後続のバージョンでも同様に適切に動作できるようにすることです。新しい機能を追加します。C や C++ などの言語の通常のパラダイムは、必要なすべてのライブラリを含む静的にリンクされた実行可能ファイルを配布することですが、.NET や Java のパラダイムは、実行時に「リンク」されるコンポーネントのコレクションとしてアプリケーションを配布することです。 .
.NET より前の COM モデルは、この一般的なアプローチを使用しようとしましたが、実際には継承がありませんでした。代わりに、各クラス定義は、すべてのパブリック メンバーを含む同じ名前のクラスとインターフェイスの両方を効果的に定義していました。インスタンスはクラス型でしたが、参照はインターフェイス型でした。クラスを別のクラスから派生したものとして宣言することは、クラスを別のインターフェイスを実装するものとして宣言することと同じであり、新しいクラスが派生元のクラスのすべてのパブリック メンバーを再実装する必要がありました。Y と Z が X から派生し、次に W が Y と Z から派生する場合、Y と Z が X のメンバーを異なる方法で実装しても問題ありません。Z はそれらの実装を使用できないためです。自分の。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 のインスタンスを作成する必要があるようです。ただし、Y と Z が実際には個別にコンパイルされたモジュールにあり、X と W がコンパイルされたときに上記のように定義されていたにもかかわらず、後で変更されて再コンパイルされたとします。Foo
X
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 を実行しようとするまで、システムのどの部分も問題があることを認識できません (ただし、Y と Z が変更される前に W が非合法であると見なされていた場合を除きます)。 .