質問

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

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

役に立ちましたか?

解決

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

2つのクラス A B があり、どちらもメソッド doSomething を定義するとしましょう。次に、 A B の両方から継承する3番目のクラス C を定義しますが、 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

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

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

クラス C A B の両方から継承する場合、コンパイラは AB 順序または BA 順序。

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

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

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

他のヒント

皆さんがおっしゃる問題は、実際にはそれほど難しくありません。実際、例えばエッフェルはそれを完璧にうまくやる! (そして、arbitrary意的な選択などを導入することなく)

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

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

ダイヤモンドの問題

  

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

     

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

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

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

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

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

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

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

適切なオブジェクト指向設計を行う場合、多重継承は必要ありません。必要な場合、通常は継承を使用して機能を再利用していますが、継承は「is-a」にのみ適しています。関係。

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

ダイアモンドの問題は問題ではないと思いますが、そのphi弁は他に何もないと考えます。

多重継承に関する私の観点から見た最悪の問題は、RAD-犠牲者と開発者であると主張しているが、実際には半分の知識で立ち往生している人々です-(最高で)。

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

public sealed class CustomerEditView : Form, MVCView<Customer>

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

私の意見では、現代言語でのコードの繰り返しはまったく必要ではなく、わずかでもないはずです。

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のインスタンスをカプセル化し、Xのメソッドの実装をそれらを通してチェーンするかもしれませんが、Xのメソッドが何をすべきかについてあいまいさはありません-それらはZのコードが明示的に指示するものは何でもします。 / p>

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

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のインスタンスを作成して、 X で定義された仮想メソッド Foo の実装を呼び出す必要があります。ただし、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()が何をすべきかが明確ではなくなりますが、ユーザーが新しいバージョンでWを実行しようとするまではYとZは、システムのどの部分でも問題があることを認識する方法はありません(YとZへの変更前でもWが不正と見なされない限り)。

ダイヤモンドは問題ありません。ただし、C ++仮想継承のようなものを使用する限り don&#8217; t :通常の継承では、各基本クラスはメンバーフィールドに似ています(実際には、 RAMをこの方法で使用すると、構文上の砂糖と、より多くの仮想メソッドをオーバーライドする追加機能が得られます。それはコンパイル時にいくらかあいまいさを課すかもしれませんが、それは通常簡単に解決できます。

一方、仮想継承を使用すると、簡単に制御不能になります(その後、混乱します)。例として&#8220; heart&#8221;図:

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

C ++では完全に不可能です。 F G が単一のクラスにマージされるとすぐに、それらの A もマージされます、期間。つまり、C ++では基本クラスを不透明と見なすことはできません(この例では、 H A を構築する必要があるため、階層のどこかに存在することを知る必要があります)。ただし、他の言語では機能する場合があります。たとえば、 F および G は、Aを&#8220; internal、&#8221;として明示的に宣言できます。したがって、結果として生じるマージを禁止し、効果的に強固にします。

別の興味深い例(ではありません C ++固有):

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

ここでは、 B のみが仮想継承を使用します。したがって、 E には、同じ A を共有する2つの B が含まれています。この方法では、 E を指す A * ポインターを取得できますが、 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の回答を参照してください)。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top