多重継承
多重継承は、オブジェクト指向のコンピュータープログラミング言語の機能であり、オブジェクトまたはクラスは、複数の親オブジェクトまたは親クラスから特性と機能を継承できます。これは、オブジェクトまたはクラスが1つの特定のオブジェクトまたはクラスからのみ継承できる単一の継承とは異なります。
多重継承は長年にわたって繊細な問題であり、対戦相手は、特定の機能がどの親クラスから継承されるのかが曖昧になる可能性がある「ダイヤモンドの問題」などの状況における複雑さと曖昧さの増加を指摘しています。親クラスはこの機能を実装します。これは、仮想継承の使用など、さまざまな方法で対処できます。あいまいさを解決するために、ミックスインや特性など、継承に基づいていないオブジェクト構成の代替方法も提案されています。
詳細
オブジェクト指向プログラミング(OOP)では、 継承は2つのクラス間の関係を表し、1つのクラス( 子クラス)が親クラスをサブクラス化します。子は親のメソッドと属性を継承し、共有機能を許可します。たとえば、摂食、繁殖などの機能を備えた変数クラスMammalを作成できます。次に、 マウスを追いかけるなどの新しい機能を追加しながら、明示的にプログラムすることなくこれらの機能を継承する子クラスCatを定義します。
多重継承により、プログラマーは複数の完全に直交する階層を同時に使用できます。たとえば、 CatがCartoonキャラクターとPet and Mammalを継承し、これらすべてのクラス内の機能にアクセスできるようにします。
実装
多重継承をサポートする言語には、C ++、Common Lisp(Common Lisp Object System(CLOS)経由)、EuLisp(The EuLisp Object System TELOS経由)、Curl、Dylan、Eiffel、Logtalk、Object REXX、Scala(mixinクラスの使用経由)が含まれます)、OCaml、Perl、Perl 6、POP-11、Python、R、およびTcl(8.6から組み込み、または以前のバージョンではIncremental Tcl(Incr Tcl)経由)。
IBM System Object Model(SOM)ランタイムは多重継承をサポートし、SOMを対象とするプログラミング言語は、複数のベースから継承された新しいSOMクラスを実装できます。
Java、C#、Rubyなどの一部のオブジェクト指向言語は、 単一の継承を実装しますが、プロトコルまたはインターフェースは、真の多重継承の機能の一部を提供します 。
PHPは、特性クラスを使用して特定のメソッド実装を継承します。 Rubyはモジュールを使用して複数のメソッドを継承します。
ダイヤモンドの問題
「 ダイアモンド問題 」(「死の死のダイアモンド」と呼ばれることもあります)は、2つのクラスBとCがAから継承し、クラスDがBとCの両方から継承する場合に発生する曖昧さです。 BとCがオーバーライドされ、Dがオーバーライドしない場合、Dが継承するメソッドのバージョンは、Bのバージョンですか、それともCのバージョンですか。
たとえば、GUIソフトウェア開発のコンテキストでは、クラスButtonはRectangle(外観用)およびClickable(機能/入力処理用)の両方のクラスから継承でき、RectangleおよびClickableクラスは両方ともObjectクラスから継承します。 Buttonオブジェクトに対してequalsメソッドが呼び出され、Buttonクラスにそのようなメソッドがなく、RectangleまたはClickable(または両方)にオーバーライドされたequalsメソッドがある場合、最終的に呼び出されるメソッドはどれですか?
この状況でのクラス継承図の形状のため、「ダイヤモンド問題」と呼ばれます。この場合、クラスAが上部にあり、BとCの両方がその下に個別にあり、Dが下部で2つを結合して菱形を形成します。
緩和
言語には、繰り返し継承されるこれらの問題に対処するさまざまな方法があります。
- デフォルトでは、C ++は各継承パスを個別にたどるので、Dオブジェクトには実際には2つの個別のAオブジェクトが含まれ、Aのメンバーの使用は適切に修飾する必要があります。 AからBへの継承とAからCへの継承が両方とも「仮想」とマークされている場合(たとえば、「クラスB:仮想パブリックA」)、C ++はAオブジェクトを1つだけ作成し、Aのメンバーを使用する正しく動作します。仮想継承と非仮想継承が混在している場合、単一の仮想Aと、Aへの各非仮想継承パスに対して非仮想Aがあります。C++では、使用する機能がWorker :: Human.Ageから呼び出される親クラスを明示的に指定する必要があります。 。 C ++は、使用するスーパークラスを限定する方法がないため、明示的な反復継承をサポートしません(つまり、1つの派生リストにクラスが複数回現れるようにする)。 C ++では、仮想継承メカニズムを介して複数のクラスの単一インスタンスを作成することもできます(つまり、Worker :: HumanとMusician :: Humanは同じオブジェクトを参照します)。
- Common Lisp CLOSは、妥当なデフォルトの動作とそれをオーバーライドする機能の両方を提供しようとします。デフォルトでは、簡単に言うと、クラス定義でBがCの前に記述されている場合、メソッドはD、B、C、Aでソートされます。最も具体的な引数クラスを持つメソッドが選択されます(D>(B、C)> A);次に、サブクラス定義で親クラスが命名された順序(B> C)。ただし、プログラマは、特定のメソッド解決順序を指定するか、メソッドを結合するためのルールを述べることにより、これをオーバーライドできます。これはメソッドの組み合わせと呼ばれ、完全に制御できます。 MOP(メタオブジェクトプロトコル)は、システムの安定性に影響を与えることなく、継承、動的ディスパッチ、クラスのインスタンス化、およびその他の内部メカニズムを変更する手段も提供します。
- Curlでは、明示的に共有としてマークされているクラスのみを繰り返し継承できます。共有クラスは、クラス内の通常のコンストラクターごとにセカンダリコンストラクターを定義する必要があります。通常のコンストラクターは、サブクラスコンストラクターを介して共有クラスの状態が初めて初期化されたときに呼び出され、他のすべてのサブクラスに対してセカンダリコンストラクターが呼び出されます。
- Eiffelでは、祖先の機能はselectおよびrenameディレクティブで明示的に選択されます。これにより、基本クラスの機能をその子孫間で共有したり、それぞれに基本クラスの個別のコピーを提供したりできます。 Eiffelでは、祖先クラスから継承された機能を明示的に結合または分離できます。 Eiffelは、機能の名前と実装が同じ場合、機能を自動的に結合します。クラスライターには、継承された機能の名前を変更してそれらを分離するオプションがあります。多重継承は、エッフェルの開発で頻繁に発生します。たとえば、広く使用されているデータ構造とアルゴリズムのEiffelBaseライブラリの有効なクラスのほとんどには、2つ以上の親があります。
- Goは、コンパイル時にダイヤモンドの問題を防ぎます。構造DがメソッドF()を持つ2つの構造BとCを埋め込み、インターフェースAを満たす場合、DF()が呼び出された場合、またはDのインスタンスが割り当てられた場合、コンパイラは「曖昧なセレクタ」について文句を言いますタイプAの変数へ。BおよびCのメソッドは、DBF()またはDCF()を使用して明示的に呼び出すことができます。
- Java 8では、インターフェイスにデフォルトのメソッドが導入されています。 A、B、Cがインターフェイスである場合、B、CはそれぞれAの抽象メソッドに異なる実装を提供でき、ダイヤモンドの問題を引き起こします。クラスDはメソッドを再実装する必要があります(その本体は呼び出しをスーパー実装の1つに単純に転送できます)。そうしないと、あいまいさがコンパイルエラーとして拒否されます。 Java 8より前は、Javaは複数の継承をサポートしておらず、インターフェースのデフォルトメソッドが利用できなかったため、Diamondの問題のリスクはありませんでした。
- バージョン1.2のJavaFX Scriptでは、ミックスインを使用して複数の継承を行うことができます。競合が発生した場合、コンパイラはあいまいな変数または関数を直接使用することを禁止します。継承された各メンバーは、オブジェクトを目的のミックスインにキャストすることでアクセスできます(例:(個人としての個人).printInfo();)。
- Logtalkは、インターフェースと実装の両方の多重継承をサポートし、デフォルトの競合解決メカニズムによってマスクされるメソッドへの名前変更とアクセスの両方を提供するメソッドエイリアスの宣言を可能にします。
- OCamlでは、親クラスはクラス定義の本文で個別に指定されます。メソッド(および属性)は同じ順序で継承され、新しく継承された各メソッドは既存のメソッドをオーバーライドします。 OCamlは、クラス継承リストの最後に一致する定義を選択して、あいまいさの下で使用するメソッド実装を解決します。デフォルトの動作をオーバーライドするには、メソッド呼び出しを目的のクラス定義で単純に修飾します。
- Perlは、クラスのリストを使用して、順序付きリストとして継承します。コンパイラは、スーパークラスリストの深さ優先検索またはクラス階層のC3線形化を使用して、最初に検出した方法を使用します。さまざまな拡張機能により、代替のクラス構成スキームが提供されます。継承の順序は、クラスのセマンティクスに影響します。上記のあいまいさでは、クラスBとその祖先はクラスCとその祖先の前にチェックされるため、AのメソッドはBを介して継承されます。これはIoとPicolispで共有されます。 Perlでは、mroまたはその他のモジュールを使用してこの動作をオーバーライドし、C3線形化またはその他のアルゴリズムを使用できます。
- Pythonの構造はPerlと同じですが、Perlとは異なり、言語の構文に含まれています。継承の順序は、クラスのセマンティクスに影響します。 Pythonは、新しいスタイルのクラスの導入時にこれに対処する必要がありました。すべてのクラスには共通の祖先オブジェクトがあります。 Pythonは、C3線形化(またはMethod Resolution Order(MRO))アルゴリズムを使用してクラスのリストを作成します。そのアルゴリズムは2つの制約を強制します:子は親の前にあり、クラスが複数のクラスから継承する場合、それらは基本クラスのタプルで指定された順序で保持されます(ただし、この場合、継承グラフの上位のクラスが下位のクラスに先行する場合がありますグラフ)。したがって、メソッド解決の順序はD、B、C、Aです。
- Rubyクラスの親は1つだけですが、複数のモジュールから継承することもできます。 rubyクラスの定義が実行され、メソッドの(再)定義は、実行時に以前に存在していた定義を覆い隠します。ランタイムメタプログラミングがない場合、これは右端の深さ優先の解決とほぼ同じセマンティクスを持ちます。
- Scalaは、 特性の複数のインスタンス化を許可します。これにより、クラス階層と特性階層間の区別を追加することにより、多重継承が可能になります。クラスは単一のクラスからのみ継承できますが、必要な数の特性を混在させることができます。 Scalaは、拡張された「特性」の右優先深さ優先検索を使用してメソッド名を解決し、結果リスト内の各モジュールの最後の出現を除くすべてを削除します。したがって、解決順序は次のとおりです。
- Tclは複数の親クラスを許可します。クラス宣言での指定の順序は、C3線形化アルゴリズムを使用するメンバーの名前解決に影響します。
クラスが1つの基本クラスからのみ派生できる単一の継承のみを許可する言語には、ダイヤモンドの問題はありません。その理由は、メソッドの繰り返しや配置に関係なく、そのような言語には継承チェーン内の任意のレベルで任意のメソッドの実装が最大で1つあるためです。通常、これらの言語により、クラスはJavaのインターフェースと呼ばれる複数のプロトコルを実装できます。これらのプロトコルはメソッドを定義しますが、具体的な実装は提供しません。この戦略は、ActionScript、C#、D、Java、Nemerle、Object Pascal、Objective-C、Smalltalk、Swift、およびPHPで使用されています。これらの言語はすべて、クラスが複数のプロトコルを実装できるようにします。
さらに、Ada、C#、Java、Object Pascal、Objective-C、Swift、およびPHPでは、インターフェイス(Objective-CおよびSwiftではプロトコルと呼ばれます)の複数の継承が可能です。インターフェイスは、動作を実装せずにメソッドシグネチャを指定する抽象基本クラスのようなものです。 (バージョン7までのJavaのような「純粋な」インターフェースは、インターフェースでの実装またはインスタンスデータを許可しません。)それでも、複数のインターフェースが同じメソッドシグネチャを宣言する場合でも、そのメソッドが実装(定義)されるとすぐに継承チェーン内の任意の場所で、その上のチェーン(スーパークラス)内のそのメソッドの実装をオーバーライドします。したがって、継承チェーンのどのレベルでも、メソッドの実装は最大で1つです。したがって、単一継承メソッドの実装では、インターフェイスが複数継承されていても、ダイヤモンドの問題は発生しません。 Java 8およびC#8のインターフェースのデフォルト実装の導入により、ダイアモンド問題を生成することは可能ですが、これはコンパイル時エラーとしてのみ表示されます。