web-dev-qa-db-ja.com

多重継承の正確な問題は何ですか?

次のバージョンのC#またはJavaに多重継承を含めるべきかどうかを常に尋ねる人々を見ることができます。この能力を持っている幸運なC++の人々は、これは最終的に自分自身を掛けるロープを誰かに与えるようなものだと言います。

多重継承の問題は何ですか?具体的なサンプルはありますか?

113
Vlad Gudim

最も明らかな問題は、関数のオーバーライドです。

2つのクラスABがあり、どちらもメソッドdoSomethingを定義するとします。次に、CAの両方から継承する3番目のクラスBを定義しますが、doSomethingメソッドをオーバーライドしません。

コンパイラがこのコードをシードすると...

C c = new C();
c.doSomething();

...メソッドのどの実装を使用する必要がありますか?それ以上の明確化がなければ、コンパイラーがあいまいさを解決することは不可能です。

オーバーライドに加えて、多重継承に関する他の大きな問題は、メモリ内の物理オブジェクトのレイアウトです。

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

コンパイラは、マシンコード(またはバイトコード)を生成するときに、それらの数値オフセットを使用して各メソッドまたはフィールドにアクセスします。

多重継承は非常に注意が必要です。

クラスCABの両方を継承する場合、コンパイラはAB順序でデータをレイアウトするか、BA順序でデータをレイアウトするかを決定する必要があります。

しかし今、あなたはBオブジェクトのメソッドを呼び出していると想像してください。本当にBですか?それとも、実際にCインターフェイスを介してBオブジェクトがポリモーフィックに呼び出されていますか?オブジェクトの実際のIDに応じて、物理的なレイアウトは異なり、呼び出しサイトで呼び出す関数のオフセットを知ることは不可能です。

この種のシステムを処理する方法は、固定レイアウトアプローチを廃止し、各オブジェクトがそのレイアウトを照会できるようにすることですbefore関数の呼び出しまたはそのフィールドへのアクセスの試行。

つまり...長い話...簡単に言えば、コンパイラの作者が多重継承をサポートするのは苦痛です。したがって、Guido van Rossumのような人がpythonを設計するとき、またはAnders Hejlsbergがc#を設計するとき、彼らは、多重継承をサポートすることでコンパイラの実装が大幅に複雑になることを知っており、おそらく利益はコストに見合うとは考えていません。

78
benjismith

あなたたちが言及する問題は、実際に解決するのはそれほど難しくありません。実際にはエッフェルはそれを完全にうまくやる! (および任意の選択などを導入することなく)

例えば。両方ともメソッドfoo()を持つAとBを継承する場合、もちろんAとBの両方を継承するクラスCでの任意の選択は望ましくありません。fooを再定義する必要があるため、使用するものが明確になります。 c.foo()が呼び出されるか、そうでなければCのメソッドの1つを名前変更する必要があります(bar()になる可能性があります)

また、多くの場合、多重継承は非常に便利だと思います。 Eiffelのライブラリを見ると、あちこちで使用されていることがわかります。個人的には、Javaでのプログラミングに戻らなければならないときに、この機能を見逃していました。

44
Daniel

ダイヤモンドの問題

2つのクラスBとCがAを継承し、クラスDがBとCの両方を継承する場合に発生するあいまいさ。BとCが overridden 、Dはそれをオーバーライドしません、そして、Dが継承するメソッドのバージョン:Bのそれ、またはCのそれ?

...この状況でのクラス継承図の形状のため、「ダイヤモンド問題」と呼ばれます。この場合、クラスAは上部にあり、BとCの両方がその下にあり、Dは下部で2つを結合して菱形を形成しています...

26
J Francis

多重継承は、頻繁に使用されないものの1つであり、誤用される可能性がありますが、必要な場合があります。

適切な代替手段がない場合に、誤用される可能性があるという理由だけで、機能を追加しないことを理解していません。インターフェイスは多重継承の代替ではありません。 1つは、前提条件または事後条件を強制できないことです。他のツールと同じように、いつ使用するのが適切か、どのように使用するかを知る必要があります。

21
KeithB

cに継承されたオブジェクトAとBがあるとします。AとBはともにfoo()を実装し、Cは実装しません。 C.foo()を呼び出します。どの実装が選択されますか?他の問題もありますが、このタイプのことは大きな問題です。

16
tloach

私はダイヤモンドの問題が問題だとは思わない。私はそのso弁について他に何も考えないと思う。

私の観点から見ると、多重継承に関する最悪の問題はRAD-犠牲者と開発者であると主張しているが、実際には半分の知識にとどまっています(せいぜい)です。

個人的には、このようなWindows Formsで最終的に何かできればとてもうれしいです(正しいコードではありませんが、アイデアが得られるはずです):

public sealed class CustomerEditView : Form, MVCView<Customer>

これは、多重継承を持たないことに関する主な問題です。インターフェースでも同様のことができますが、「s ***コード」と呼ばれるものがあります。たとえば、データコンテキストを取得するために各クラスに記述する必要があるこの苦痛な繰り返しc ***です。

私の意見では、最新の言語でコードを繰り返す必要はまったくなく、まったく必要ではないはずです。

5
Turing Complete

多重継承の主な問題は、tloachの例でうまくまとめられています。同じ関数またはフィールドを実装する複数の基本クラスから継承する場合、コンパイラはどの実装を継承するかを決定する必要があります。

これは、同じ基本クラスを継承する複数のクラスから継承すると悪化します。 (ダイヤモンドの継承、継承ツリーを描くとダイヤモンドの形になります)

これらの問題は、コンパイラーが克服するのに本当に問題ではありません。ただし、コンパイラがここで選択しなければならない選択はかなり、意的であり、これによりコードがはるかに直感的ではなくなります。

良いOOデザインをするとき、多重継承は必要ありません。それが必要な場合、通常、継承は機能を再利用するために使用されていますが、継承は "is- a」関係。

同じ問題を解決し、多重継承の問題を持たないミックスインのような他のテクニックがあります。

5
Mendelt

Common LISP Object System(CLOS)は、C++スタイルの問題を回避しながらMIをサポートするものの別の例です。継承には sensible default が与えられますが、 、たとえば、スーパーの動作を呼び出します。

3
Frank Shearar

Java and .NETなどのフレームワークの設計目標の1つは、コンパイル済みのコードが事前にコンパイルされたライブラリの1つのバージョンで動作し、後続のバージョンでも同様に動作することを可能にすることですCやC++などの言語の通常のパラダイムは、必要なすべてのライブラリを含む静的にリンクされた実行可能ファイルを配布することですが、.NETおよびJavaは、実行時に「リンク」されるコンポーネントのコレクションとしてアプリケーションを配布します。

.NETに先行するCOMモデルはこの一般的なアプローチを使用しようとしましたが、実際には継承はありませんでした。代わりに、各クラス定義は、クラスとそのすべてのパブリックメンバーを含む同じ名前のインターフェイスの両方を効果的に定義しました。インスタンスはクラス型であり、参照はインターフェース型でした。クラスを別のクラスから派生すると宣言することは、クラスを他のインターフェイスの実装として宣言することと同等であり、新しいクラスが派生したクラスのすべてのパブリックメンバーを再実装する必要がありました。 YとZがXから派生し、次にWがYとZから派生する場合、YとZがXのメンバーを異なる方法で実装するかどうかは問題になりません。Zはその実装を使用できないためです。自分の。 WはYおよび/またはZのインスタンスをカプセル化し、Xのメソッドの実装をそれらのメソッドにチェーンしますが、Xのメソッドが何をすべきかについてあいまいさはありません-それらはZのコードが明示的に指示するものは何でもします。

Java and .NETの難点は、コードがメンバーを継承し、それらにアクセスできることです暗黙的には親メンバーを参照します。上記:

_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を実行しようとするまで、どの部分もありませんシステムの問題は、問題があったことを認識することができました(YとZへの変更の前でさえ、Wが不正であると見なされた場合を除く)。

2
supercat

多重継承自体に問題はありません。問題は、最初から多重継承を念頭に置いて設計されていない言語に多重継承を追加することです。

Eiffel言語は、非常に効率的かつ生産的な方法で制限なしに多重継承をサポートしていますが、言語はそれをサポートするために最初から設計されました。

この機能はコンパイラ開発者向けに実装するのは複雑ですが、その欠点は、適切な多重継承サポートが他の機能のサポートを回避できるという事実(つまり、インターフェースまたは拡張メソッドの必要性)によって補われると思われます。

多重継承をサポートするかどうかは、選択の問題であり、優先順位の問題だと思います。より複雑な機能は、正しく実装されて動作するまでに時間がかかり、より物議をかもします。 C++実装は、C#およびJavaで多重継承が実装されなかった理由かもしれません...

2
Christian Lemer

do n't C++仮想継承のようなものを使用する限り、ダイアモンドは問題ではありません。通常の継承では、各基本クラスはメンバーフィールドに似ています(実際には、RAM this way)、構文上の砂糖と、より多くの仮想メソッドをオーバーライドする追加の機能を提供します。これにより、コンパイル時に多少のあいまいさが生じる可能性がありますが、通常は簡単に解決できます。

一方、仮想継承を使用すると、簡単に制御不能になります(そして混乱になります)。例として、「ハート」図を考えてみましょう。

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

C++では、FGが単一のクラスにマージされるとすぐに、それらのAsもピリオドにマージされます。つまり、C++では基本クラスを不透明と見なすことはできません(この例では、AHを構築する必要があるため、階層のどこかに存在することを知る必要があります)。ただし、他の言語では機能する場合があります。たとえば、FGは、Aを明示的に「内部」として宣言することができます。これにより、結果として生じるマージを禁止し、効果的に強固にします。

別の興味深い例(not C++固有):

  A
 / \
B   B
|   |
C   D
 \ /
  E

ここでは、Bのみが仮想継承を使用します。したがって、Eには、同じBを共有する2つのAが含まれます。この方法では、Eを指すA*ポインターを取得できますが、実際にはオブジェクトisですが、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;
}

さらに、実装は非常に複雑になる場合があります(言語によって異なります。benjismithの回答を参照してください)。

2
number Zero