継承 (プログラミング)

オブジェクト指向を構成する概念の一つ
多重継承から転送)

コンピュータプログラミングにおける継承(けいしょう、: inheritance)とは、任意のオブジェクトの特性を、他のオブジェクトの特性の基礎にするためのメカニズムと定義されている。

基礎にされる継承元は親、その継承先は子と呼ばれて、状態機能定数注釈などが引き継がれるが、コンストラクタデストラクタは対象外になる。その親と子の関係を、クラスベースOOPはスーパークラスサブクラスの関係で、プロトタイプベースOOPはプロトタイプとクローンの関係で導入している[1]

概要

編集
 
継承図

継承は、他のオブジェクトの特性(データ手続き関数定数アノテーションなど)を引き継ぐという概念であり、引き継いだオブジェクトがどのような性質を持ち、どのように振る舞うのかは全くの任意になる。引き継ぎかたは、リクエストされた特性をそのオブジェクトが持たない場合は、自動的に上位オブジェクトの方でサーチするという方式が一般的であり、これは暗黙の委譲(delegation)ベースとも呼ばれる。他には、インスタンス化時にその型の継承チェーンを走査してその全要素を集めて同名重複要素を解決して1つの実体を生成するという方式もあり、これは連結(concatenation)ベースとも呼ばれる。

他オブジェクトの特性を引き継ぐという概念は、それに新しい特性群を付け足しての手軽なオブジェクトの機能拡張と、引き継がれる共通の特性群を上位ノードにしたオブジェクトの分類体系化をもたらしている。これは差分プログラミングとも呼ばれ、プログラムの再利用性と保守性を高めるとされている。

継承(inheritance)とサブタイピング(subtyping)は混同されやすい。ここでのサブタイピングは、親オブジェクトに対する子オブジェクトの安全な代替/代入(substitute)を保証する継承という意味で使われている。それに対してのただの継承は、親オブジェクトの特性をただ引き継ぐことに専念しており、安全な代替/代入には無関心である。たとえ話としては、親の白黒映画をカラー映画化するのが代替可能なサブタイピングであり、親の白黒映画をメディアミックス的グッズ販売につなげるのが代替不可な継承になる。継承に代入可能性(substitutability)を順守させてサブタイピングにすることを提唱しているのが、リスコフの置換原則である。

継承と対比される概念にコンポジション (合成)英語版がある。継承のサブタイピング用法の上位概念と下位概念Is-a)に対して、合成は(Has-a)であるが、継承の非サブタイピング用法では、スーパークラスとサブクラスの関係が(Is-aでもHas-aでもない)の関係になることがしばしばあるので、それと合成との使い分けが重視されるようになっている。

継承の目的

編集

差分プログラミング

編集

差分プログラミング(difference coding)とは、クラス間の共通構成を、各クラスの特有構成に引き継がせるようにして、重複構成の削減と、分類体系化をもたらすことを目的にした継承の用法である。これは、クラスに新機能を付け足しての手軽なクラス拡張目的と、クラスの共通部分を括りだして体系化するクラス分類目的の双方に使われた。

差分プログラミングは、継承の元々の用法であり、プログラムの再利用性と保守性を高めると見なされていたが、後年になると階層分散配置されたデータとメソッドの把握のしづらさによる弊害の方が目立つようになって、この用法を否定する傾向が強くなった。同時にその代替としての合成英語版が重視されるようになっている。

サブタイピング

編集

サブタイピング(subtyping)とは、スーパークラスのインスタンスを、サブクラスのインスタンスで安全に代替できることを指針にした継承の用法である。基底クラスの変数への、派生クラスのインスタンスの安全な代入可能性(substitutability)を保証している。これはIs-a関係とも言われる。サブタイピングでは、派生側でのフィールドの追加は抑制され、基底側からのメソッド実装の引き継ぎも抑制されており、基底側からのメソッド定義(メソッドシグネチャ)の引き継ぎが重視されている。派生インスタンスが代入された基底変数のメソッド名から派生メソッド内容が呼び出される言語機能は、メソッドオーバーライドと呼ばれ、その機能概念は動的ディスパッチ英語版と呼ばれる。サブタイピングは動的ディスパッチに焦点を当てた継承と解釈できる。

具象メソッド(定義+実装)の引き継ぎは実装継承(implementation inheritance)またはコード継承(code inheritance)と呼ばれており、抽象メソッド(定義だけ)の引き継ぎは界面継承(interface inheritance)と呼ばれている。

Is-a関係サブタイピング主体の継承関係は、UMLクラス図では汎化/特化の関係に投影されている。抽象メソッドだけで構成される純粋抽象クラスは、インターフェースと呼ばれており、それとの継承関係はUMLクラス図では実現/実装の関係に投影されている。

サブタイピングのコーディング例はこうなる。

#include <iostream>
#include <string>
#include <typeinfo>

class Base {
public:
    virtual ~Base() {}
    virtual std::string greet() const = 0;
};

class Derived : public Base {
    virtual ~Derived() { std::cout << "Destructor of Derived is called." << std::endl; }
    virtual std::string greet() const { return "Hello!"; }
};

int main() {
    Base* b = new Derived(); // OK
    std::cout << "Message: " << b->greet() << std::endl;
    std::cout << "Is instance of Derived? " << std::boolalpha << (typeid(*b) == typeid(Derived)) << std::endl;
    delete b;
    return 0;
}

多重継承

編集

クラスに複数のスーパークラスを持たせることを多重継承という。単一継承と異なり、多重継承では、スーパークラス上のメンバサーチが複数方向に分かれるので、どのメンバが参照されるのかの把握が困難になるという欠点がある。特にフィールドの多重継承・分散配置は、早期に原則禁止が一般化している。メソッドの方はやむなく許容されたので、メソッド決定順序(MRO)問題が取り沙汰された。MRO問題を解決するために導入されたのが、インターフェースの実装やトレイトのインクルードであり、双方はデータ主体クラスを単一継承にしてメソッド主体クラスを多重継承にするというハイブリッド継承の担い手になった。

また、多重継承上のスーパークラスの重複による菱形継承問題も問題視されるようになっている。菱形継承問題の解決策としては、C++/Eiffel発の仮想継承Eiffel発のリネーミング、Python発のC3線形化などがある。

多重継承と仮想継承のコーディング例を以下に示す。同一のクラスから継承している複数の派生クラスを多重継承して1つのクラスを作る場合に始めの基底クラスの存在をどうするかによって仮想継承と通常の多重継承の2つに分かれる。

class Base {
public:
    int n;
};

// 非仮想継承。
class DerivedNV1 : public Base { /* ... */ };
class DerivedNV2 : public Base { /* ... */ };

// 仮想継承。
class DerivedV1 : public virtual Base { /* ... */ };
class DerivedV2 : public virtual Base { /* ... */ };

class DerivedNV : public DerivedNV1, public DerivedNV2 { /* ... */ };
class DerivedV : public DerivedV1, public DerivedV2 { /* ... */ };

int main() {
    DerivedNV nv;
    //nv.n = 0; // 曖昧さが解決できないためコンパイルエラー。
    nv.DerivedNV1::n = 0;
    nv.DerivedNV2::n = 0;
    DerivedV v;
    v.n = 0; // コンパイルエラーにはならない。
    return 0;
}

この例のような状態は特に菱形継承(ダイアモンド継承)と呼ばれる。

仮想継承でない場合、DerivedNVインスタンスにはDerivedNV1の基底のBase::nDerivedNV2の基底のBase::nという2つのnが別に存在することになる(メンバ関数も同様)。一方、仮想継承した場合、DerivedVのインスタンスにはBaseの部分はただ1つしか存在しない。DerivedV1の基底とDerivedV2の基底が共有されている状態である。

先発OOP言語のC++Eiffelでは実装の多重継承ができたが、後発言語のJavaC#では実装は単一継承限定にされ、代わりにインターフェースの多重継承(界面の多重継承)が導入されている。なぜなら実装の多重継承はメリットよりもデメリットのほうが多いとみなされたためである。

  1. 継承関係が複雑になるため全体の把握が困難になる。
  2. 名前の衝突。同じ名前を複数の基底クラスがそれぞれ別の意味で用いていた場合、その両方を派生クラスでオーバーライドするのが困難。
  3. 処理系の実装が複雑になってしまう。
  4. 仮想継承にしていない場合に同一の基底クラスが複数存在してしまう(これが望ましい場面もあるが)。これの何が問題かというと、最初は仮想継承していなかったものを、後から仮想継承にしたくなったときに、変更点を洗い出すのが大変になるからである。つまり仮想継承を使用するには設計をきちんと行う必要があるということである。

しかしながら多重継承を使う方が直感的になる場合もあるとの主張もあり、どちらが正しいとは言えない状況である。

カプセル化の可視性と継承の可視性

編集

カプセル化の可視性(public/protected/package/private)によって、各スーパークラスメンバの受け継ぎが取捨選択されることは、派生型に対する継承の大きな特徴である。privateメンバはサブクラスに受け継がれない。packageメンバは外部パッケージのサブクラスには受け継がれない。

継承の可視性は、スーパークラスメンバ(フィールド/メソッド)の可視性に更に制約をかける機能である。三段階ある。

  1. public継承 - そのままの継承。
  2. protected継承 - スーパークラスのpublicメンバを、protectedメンバに引き下げて継承する。
  3. private継承 - スーパークラスのpublic/protectedメンバを、privateメンバに引き下げて継承する。

これはC++で導入されていたが、後継OOP言語ではほとんど採用されていない。

ミックスイン

編集

ミックスイン(Mix-in)は、多重継承問題の解決策を発端にしたもう1つの継承方法論である。メソッドの集合体を継承することで、その機能をクラスに注入することを目的にしており、メソッド集合体とクラスの間には汎化/特化の関係がないままで、多重継承を前提にしている。そのメソッド集合体はトレイトとされることが多く、他にモジュール、プロトコル、ロールといった形態もある。トレイトの継承はインクルードと呼ぶのが好まれ多重継承前提である。ミックスインはそれらをひっくるめた方法論としての用語になっている。

界面継承のインターフェース(抽象メソッドをまとめたクラス)と、Mix-in継承のトレイト(独立メソッドをまとめたモジュール)は双方とも多重継承前提なのでよく対比されて説明される。双方の違いを列挙すると以下のようになる。

  • 界面継承は抽象メソッドをクラスに相続させるのに対して、Mix-in継承は独立メソッドをクラスに贈与する。
  • 界面継承はインターフェースの継承先クラスに実装メソッドを記述するが、Mix-in継承はトレイトに実装メソッドを記述する。
  • 界面継承は同名アルゴリズムを個々のクラスのメソッドに分散記述するが、Mix-in継承は1つのメソッドに個々のクラスのアルゴリズムを一括記述する。
  • インターフェースはデータメンバを持つことを想定されていないが、トレイトはデータメンバを持つ。すなわち界面継承は派生メソッドたち専用の共有データを持てないが、Mix-in継承はそれが可能である。
  • 界面継承はthis参照を暗黙使用できるが、Mix-in継承は不可なのでThis参照の明示的な引数渡しや関連型の機能が必要になる。
  • インターフェースは記名的型付けであるのに対して、トレイトは構造的型付けで識別されることが多い。

UMLにおける継承

編集

統一モデリング言語 (UML) のクラス図では、サブクラスから見たスーパークラスは汎化 (generalization) 、スーパークラスから見たサブクラスは特化 (specialization) と呼ばれる。

純粋抽象クラスはインターフェースと定義されており、クラスから見たインターフェースは実現(realization)[要検証]、クラスがインターフェースを継承することは実装(implementation)と呼ばれる。

サブタイピング用法の投影は汎化/特化の関係であり、インターフェース用法の投影は実現/実装の関係である。ミックスイン用法はUMLクラス図で扱われていない関係であり、差分プログラミング用法も同様である。

脚注

編集
  1. ^ MDN contributors (2022年9月17日). "継承とプロトタイプチェーン - JavaScript". developer.mozilla.org. 2022年9月18日閲覧

関連項目

編集