菱形継承問題

出典: フリー百科事典『ウィキペディア(Wikipedia)』
菱形継承の概念図

菱形継承問題(ひしがたけいしょうもんだい、: diamond problem)は、多重継承を伴うオブジェクト指向プログラミング言語において、クラス A を2つのクラス B と C が継承し、B と C の両方をクラス D が継承する際に発生するあいまいさを指す用語である。たとえば、クラス D にあるメソッドが A で定義された(かつ D においてオーバーライドされていない)メソッドを呼び出すとしたとき、B と C がそのメソッドを異なった形でオーバーライドしていたら、D は B と C のどちらのメソッドを継承するのか、という問題がある[1]

例えば、クラス Button は クラス Rectangle(見た目のため)と Mouse(マウスイベントのため)を継承し、Rectangle も Mouse も Object クラスを継承しているとする。ここで Button オブジェクトが equals メソッドを呼び出し、Button クラス自体にはそのメソッドは定義されていないとする。Rectangle と Mouse にはオーバーライドされた equals メソッドがそれぞれ定義されているとしたら、どちらを呼び出すべきか?

これが「菱形; diamond」問題と呼ばれるのは、クラス継承図の形状が菱形になるためである。クラス A が頂上にあり、B と C がそれぞれそこから枝分かれし、D がその2つの枝を再び1つにすることで、全体として菱形を形成する。

いかにも難問のような雰囲気があるが、継承によるオブジェクト指向設計ではごくあたりまえに考えられうる形である。たとえば、ドローソフトにおける各種の図形を扱うクラスを設計している、としよう。「図形」→「四角形」→「平行四辺形」と派生させ、「平行四辺形」から「長方形」や「菱形」を派生させる。ここで「正方形」は長方形であると同時に菱形でもある、という形で菱形継承があらわれる(なお、ここでいう図形の菱形は、英: rhombus であり「ダイヤモンド」ではなく、日本語の偶然である[疑問点])。またストリームなどでも、「読み出しストリーム」「書き込みストリーム」の両方を継承した「読み書きストリーム」といった形であらわれる。

対処法[編集]

プログラミング言語ごとにこの問題への対処法は異なる。

  • C++ では、デフォルトでは個々の継承経路を独立して扱う。従って D オブジェクトには実際には2つの独立した A オブジェクトが内包され、A のメンバの使用は適切に行われる。A から B への継承と A から C への継承が共に "virtual"(例えば "class B : virtual public A")である場合、C++ はこれを特別に扱い、1つの A オブジェクトだけを生成し、A のメンバは正しく動作する。仮想継承と仮想でない継承が混在した場合、唯一の仮想の A と個々の仮想でない継承経路ごとの A が存在することになる。
  • Common Lisp では、合理的なデフォルトの動作とそれをオーバーライドする能力を提供する。デフォルトでは、引数のクラス指定が最も具体的なメソッドが選択され、サブクラスの定義内でスーパークラスが指定された順番に従う。しかし、プログラマはこれをオーバーライドでき、メソッドごとの解決順序を指定したり、メソッド結合規則を指定したりできる。
  • Eiffel では、ディレクティブを改名して選択することでこの問題を回避する。すなわち、上位クラスのメソッドを下位オブジェクトが使うときは明示的に指定する。これによって基底クラスのメソッド群がサブクラス間で共有でき、個々のクラスが基底クラスの個別のコピーを持っているように見なせる。
  • PerlIo では、継承するクラス群を順序リストで指定することで対処する。上述の例で言えば、クラス B の上位の方がクラス C の上位の前にチェックされるので、A のメソッドは B を通してのみ継承される。
  • 2.1 以前の Python では、多重継承に対し、深さ優先-左から右の順でクラスのリストを生成する。Python 2.2 で導入され Python 3 では統一された、新スタイルクラス[2]では、全てのクラスは共通の基底クラス object から派生させるため、菱形継承への対処が重要になった。この時に同時に導入された順序は 2.2 でのみの採用にとどまったためここでは説明しない[3]。Python 2.3 以降および Python 3 では C3(w:C3 linearization)が採用された[4]

その他の例[編集]

クラスの多重継承ができない言語のうち、(Objective-CPHPC#Javaなど)実装を持たないインタフェースのみを多重継承可能にしている言語がある(Objective-C ではプロトコルと呼ぶ)。実装を持たないため、インタフェースを多重継承しても、特定のメソッドやメンバ変数には常に1つの実装しかないので、あいまいさは発生しない。

Rubyは次のようなMixinアプローチにより菱形問題を回避している。クラスはクラスを単一継承し、クラスを多重継承することはできない。Rubyにはクラスの他にモジュールがあり、クラスはモジュールを多重継承することができる。モジュールには継承関係が無いので、菱形問題は発生しない。なお、クラスのクラス「Class」はモジュールのクラス「Module」のサブクラスである。

菱形問題は継承に限ったことではない。A、B、C、D というヘッダファイルが互いに菱形を形成するように "#include" されている場合、同様の問題が発生しうる。プリプロセッサで処理された結果、A にあった宣言が B と C で異なった形に変えられ、"#ifdef" が適切に機能しないという状況がありうる。同様に、ミドルウェアスタックでも似たような問題が発生する。A がデータベース、B と C がそのキャッシュだとした場合、D が B と C にトランザクションコミットを要求すると、A にはコミット要求が重複して届いてしまう。

[編集]

  1. ^ 他にもたとえば実装の観点からは、vtblの設計が難しくなるという問題などもある。
  2. ^ http://www.python.org/doc/newstyle/
  3. ^ 詳細は https://python-history.blogspot.com/2010/06/method-resolution-order.html を参照のこと
  4. ^ http://www.python.org/download/releases/2.3/mro/

関連項目[編集]