菱形継承問題
出典: フリー百科事典『ウィキペディア(Wikipedia)』
菱形継承問題(英: Diamond problem)とは、多重継承を伴うオブジェクト指向プログラミング言語において、クラス A を2つのクラス B と C が継承し、B と C の両方をクラス D が継承する際に発生するあいまいさを指す用語である。クラス D にあるメソッドが A で定義された(かつ D においてオーバーライドされていない)メソッドを呼び出すとしたとき、B と C がそのメソッドを異なった形でオーバーライドしていたら、D は B と C のどちらのメソッドを継承するのか、という問題である。
例えば、クラス Button は クラス Rectangle(見た目のため)と Mouse(マウスイベントのため)を継承し、Rectangle も Mouse も Object クラスを継承しているとする。ここで Button オブジェクトが equals メソッドを呼び出し、Button クラス自体にはそのメソッドは定義されていないとする。Rectangle と Mouse にはオーバーライドされた equals メソッドがそれぞれ定義されているとしたら、どちらを呼び出すべきか?
これが「菱形; diamond」問題と呼ばれるのは、クラス継承図の形状が菱形になるためである。クラス A が頂上にあり、B と C がそれぞれそこから枝分かれし、D がその2つの枝を再び1つにすることで、全体として菱形を形成する。
[編集] 対処法
プログラミング言語毎にこの問題への対処法は異なる。
- C++ では、デフォルトでは個々の継承経路を独立して扱う。従って
D
オブジェクトには実際には2つの独立したA
オブジェクトが内包され、A
のメンバの使用は適切に行われる。A
からB
への継承とA
からC
への継承が共に "virtual
"(例えば "class B : virtual public A
")である場合、C++ はこれを特別に扱い、1つのA
オブジェクトだけを生成し、A
のメンバは正しく動作する。仮想継承と仮想でない継承が混在した場合、唯一の仮想のA
と個々の仮想でない継承経路ごとのA
が存在することになる。 - Common Lisp では、合理的なデフォルトの動作とそれをオーバーライドする能力を提供する。デフォルトでは、引数のクラス指定が最も具体的なメソッドが選択され、サブクラスの定義内でスーパークラスが指定された順番に従う。しかし、プログラマはこれをオーバーライドでき、メソッドごとの解決順序を指定したり、メソッド結合規則を指定したりできる。
- Eiffel では、ディレクティブを改名して選択することでこの問題を回避する。すなわち、上位クラスのメソッドを下位オブジェクトが使うときは明示的に指定する。これによって基底クラスのメソッド群がサブクラス間で共有でき、個々のクラスが基底クラスの個別のコピーを持っているように見なせる。
- Perl や Io では、継承するクラス群を順序リストで指定することで対処する。上述の例で言えば、クラス
B
の上位の方がクラスC
の上位の前にチェックされるので、A
のメソッドはB
を通してのみ継承される。 - Python では、新たな形式のクラス群を導入してこれに対処している。全てのクラスは共通の基底クラス
object
から派生している。Python は左優先・深さ優先のクラスのリストを生成する(上述の例の場合、D
,B
,A
,C
,A
)。そして、同じクラスが複数回出現するときは最後に出現する箇所を除いて他を削除する(従って、上述の場合、D
,B
,C
,A
となる)。
[編集] その他の例
多重継承ができない言語(Objective-C、PHP、C#、Java)ではインタフェースの多重継承が可能である(Objective-C ではプロトコルと呼ぶ)。インタフェースは基本的には抽象基底クラスであり、抽象メソッドからなる(データメンバを持たない)。従って特定のメソッドやメンバ変数には常に1つの実装しかないので、あいまいさは発生しない。
菱形問題は継承に限ったことではない。A、B、C、D というヘッダファイルが互いに菱形を形成するように "#include" されている場合、同様の問題が発生しうる。プリプロセッサで処理された結果、A にあった宣言が B と C で異なった形に変えられ、"#ifdef" が適切に機能しないという状況がありうる。同様に、ミドルウェアスタックでも似たような問題が発生する。A がデータベース、B と C がそのキャッシュだとした場合、D が B と C にトランザクションのコミットを要求すると、A にはコミット要求が重複して届いてしまう。