継承 (プログラミング)

出典: フリー百科事典『ウィキペディア(Wikipedia)』

これはこのページの過去の版です。2400:4053:2042:db00:5d32:fdf4:479f:c71d (会話) による 2020年7月31日 (金) 11:44個人設定で未設定ならUTC)時点の版であり、現在の版とは大きく異なる場合があります。

継承(けいしょう、: inheritance、インヘリタンス)とはオブジェクト指向を構成する概念の一つである。あるオブジェクトが他のオブジェクトの特性を引き継ぐ場合、両者の間に「継承関係」があると言われる。

主にクラスベースオブジェクト指向言語で、既存クラスの機能、構造を共有する新たなクラス(派生クラス、派生型)を定義すること(subclassingサブクラス化)ができる。またそのような派生クラスは「親クラス(スーパークラス)を継承した」という。具体的には変数定義(フィールド)や操作(メソッド)などが引き継がれる。またJavaC#などのインタフェース実装のように、機能セットの仕様(プロトコル)のみを引き継ぐ場合もある。

一般的に、BがAを継承する場合、"B is a A."(BはAの一種である)という意味的な関係(Is-a関係)が成り立つ。従って、同じふるまいを持つからと言って、意味的に無関係なクラス間に継承関係を持たせるのは適切でない場合が多い。

プロトタイプベースのオブジェクト指向言語(SelfNewtonScript等)では「クラス」の概念を持たないが、クローン(複製)元となるオブジェクトからプロパティを受け継ぐことを「継承」と呼ぶこともある[1]

継承と類似の概念に「委譲」があるが、継承では一度定まった継承関係は通常変更されないのに対して、委譲対象は必要に応じて変更されうるものである。

Is-a関係を持つ継承とは階層が異なる概念として集約 (aggregation) とコンポジション集約 (composition) があるが、これはクラス間の関係がHas-aである包含関係であり、クラス間の関係は継承よりもである。

継承をする意義

継承をする意義は、主に以下の目的によるものである。

コードの再利用

継承では、スーパークラスの構造と機能がサブクラスにそのまま引き継がれるため、サブクラスでスーパークラスのコードを再利用できる。例えば、スーパークラスでハッシュテーブルの操作をする機能があった場合、サブクラスではその機能を再利用し、その上に任意のデータをハッシュ化して格納するといったような機能を追加するという具合である。

これにより、一連の操作を抽象化したまま機能の追加を行うことができ、コードの再利用性が向上する。通例、スーパークラスではより汎用的で抽象的な機能を提供し、サブクラスではより特化した具体的な機能を提供する。

派生型による共用と置換、多態性

あるスーパークラスを継承したサブクラスは、そのスーパークラスとして振る舞うこともできる。つまり、そのスーパークラスと同等の型としての使用が可能になる。

通常、静的型付けの言語では、変数には同じ型の値(もしくはインスタンス)しか格納することはできないが、継承を利用した場合では、スーパークラス型の変数(参照型変数あるいはポインタ型変数)に、そのスーパークラスを継承したサブクラスのインスタンスを格納することができる(ただしC++のprivate継承のような例外もある)。

これにより、あるスーパークラスのコレクション(配列連結リストなど)に、派生したサブクラス群を一緒に格納しておく、といったことが可能となる(ただし、スーパークラスで仮想化されておらず、サブクラスのみで追加された機能を利用するときは、適切なダウンキャストが必要となる)。また、あるスーパークラス型のインスタンスを引数として受け取る関数を使用する場合に、派生したサブクラス型のインスタンスを渡すことも可能となる(リスコフの置換原則)。

しかし、継承の最大の意義は多態性ポリモーフィズム)にある。スーパークラスで定義した仮想関数(仮想メソッド)を、サブクラスでオーバーライドすることで、動的な多態性を実現することができる。

#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;
}

多重継承と仮想継承

複数のクラスから継承することを多重継承という。多重継承のバリエーションとして仮想継承がある。同一のクラスから継承している複数の派生クラスを多重継承して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の基底が共有されている状態である。

C++ではクラスの多重継承(実装の多重継承)・仮想継承が共に使用できる。しかし後発の言語であるJavaC#D言語ではいずれも使用できない。代わりに単独継承と0個以上のインタフェース実装(型の多重継承)を用意している。なぜなら実装の多重継承はメリットよりもデメリットのほうが多いとみなされたためである。

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

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

限定多重継承

完全な実装多重継承が問題を起こしやすいのは、継承という仕組みの関係上複数の親クラスが対等に扱われる事が原因である。そこで完全な継承を行なう代わりに能力を限定した実装の引き継ぎを行なう手法もある。これらはモジュール、トレイトなどと呼ばれ、通常の継承とは区別される。継承との違いは次のようなものである。

  1. インスタンス変数を持たない。つまり特定の構造に依存しない純粋なメソッド定義を行なう。
  2. メソッドの集合演算が定義できる。
  3. 必要に応じてクラスに付加される。クラスの通常継承時にそれらを引き継ぐ必要はない。
  4. 通常の継承とは独立の継承構造を持つため構造の把握が行ないやすい。意味のつながりを持たないクラス間で横断的に定義されるメソッドで特に有効である。

これらの特徴から多重継承の問題のうち1,2,4はほぼ解決できる。一般には多重継承を行なう場合も、このような使い方をする事が望ましいと考えられている。

UMLにおける継承

統一モデリング言語 (UML) のクラス図においては、BがAを継承する場合、AとBの間には汎化 (generalization) の関係があるという。同時に、AはBを汎化したクラスであるといえる。逆に、BはAを特化 (specialization) したクラスであるともいえる。視覚的な記述方法としては、AとBを線で結び、A側の線の端に白抜きの三角を描くことで表現する。

脚注

関連項目