継承 (プログラミング)
継承(けいしょう、inheritance:インヘリタンス)とはオブジェクト指向を構成する概念の一つである。あるオブジェクトが他のオブジェクトの特性を引き継ぐ場合、両者の間に「継承関係」があると言われる。
主にクラスベースのオブジェクト指向言語で、既存クラスの機能、構造を共有する新たなクラスを派生することができ(サブクラス化)、そのようなクラスは「親クラス(スーパークラス)を継承した」という。具体的には変数定義や操作(メソッド)などが引き継がれる。またJavaのインタフェース継承のように機能セットの仕様のみを引き継ぐ場合もある。
一般的に、BがAを継承する場合、B is a A. (BはAの一種である)という意味的な関係(Is-a関係)が成り立つ。従って、同じふるまいを持つからと言って、意味的に無関係なクラス間に継承関係を持たせるのは適切でない場合が多い。
プロトタイプベースのオブジェクト指向言語(Self、NewtonScript等)のように「クラス」という概念を持たない場合でも、クローン元となるオブジェクトを指して「継承」と呼ぶ。
継承と類似の概念に「委譲」があるが、継承では一度定まった継承関係は通常変更されないのに対して、委譲対象は必要に応じて変更されうるものである。
Is-a関係を持つ継承とは階層が異なる概念として集約 (aggregation) とコンポジション集約 (composition) があるが、これはクラス間の関係がHas-aである包含関係であり、クラス間の関係は継承よりも疎である。
継承をする意義
継承をする意義は、主に以下の目的による物である。
コードの再利用
継承では、スーパークラスの構造と機能がサブクラスにそのまま引き継がれるため、サブクラスでスーパークラスのコードを再利用できる。
例えば、スーパークラスでハッシュテーブルの操作をする機能があった場合、サブクラスではその機能を再利用し、その上に任意のデータをハッシュ化して格納するといったような機能を追加するという具合である。
これにより、一連の操作を抽象化したまま機能の追加を行う事ができ、コードの再利用性が向上する。
派生型による共用と置換
テンプレート:Main あるスーパークラスを継承したサブクラスは、そのスーパークラスと同等の型としての使用が可能になる。
通常、静的型付けの言語では、変数には同じ型の値(もしくはインスタンス)しか格納する事はできないが、継承を利用した場合では、スーパークラスで型付けされた変数に、そのスーパークラスを継承したサブクラスのインスタンスを格納することができる。(C++のprivate継承のような例外もある)
これにより、あるスーパークラスの配列に、派生したサブクラス群を一緒に格納しておく、といった共用が可能である。(ただし、サブクラスで追加された構造を扱う時は、適切なダウンキャストが必要である)
そして、あるスーパークラスを引数で受け取る関数を使用する場合に、派生したサブクラスを渡す置換も可能である。
また、あるインスタンスが何らかの型と同一か検査する場合に、派生したサブクラスが派生元のスーパークラスを同一の物と判断されるようになる。
class Base{};
class Derivation : public Base{};
int main()
{
Base* b = new Derivation(); //OK
return 0;
}
多重継承と仮想継承
複数のクラスから継承することを多重継承という。多重継承のバリエーションとして仮想継承がある。同一のクラスから継承している複数の派生クラスを多重継承して1つのクラスを作る場合に始めの基底クラスの存在をどうするかによって仮想継承と通常の多重継承の2つに分かれる。
class Base
{
public:
int n;
};
//仮想継承
class D1 : public virtual Base { /* ~ */ };
class D2 : public virtual Base { /* ~ */ };
class Derivation : public D1, public D2 { /* ~ */ };
この例のような状態は特に菱形継承(ダイアモンド継承)と呼ばれる。
仮想継承でない(D1, D2の部分のvirtualを取り除く)場合、DerivationのインスタンスにはD1の基底のBaseのnとD2の基底のBaseのnという2つのnが別に存在することになる(メンバ関数も同様)。仮想継承した場合、DerivationのインスタンスにはBaseの部分はただ1つしか存在しない。D1の基底とD2の基底が共有されている状態である。
C++ではクラスの多重継承(実装多重継承)・仮想継承が共に使用できる。しかしC++を基にしているJava、C#、Dではいずれも使用できない。代わりに単独継承と0個以上のインタフェース実装を用意している。なぜなら多重継承は問題点が多いと思われたためである。
- 継承関係が複雑になるため全体の把握が困難になる。
- 名前の衝突。同じ名前を複数の基底クラスがそれぞれ別の意味で用いていた場合、その両方を派生クラスでオーバーライドするのが困難。
- 処理系の実装が複雑になってしまう。
- 仮想継承にしていない場合に同一の基底クラスが複数存在してしまう(これが望ましい場面もあるが)。
- これの何が問題かというと、最初は仮想継承していなかったものを、後から仮想継承にしたくなったときに、変更点を洗い出すのが大変になるからである。つまり仮想継承を使用するには設計をきちんと行う必要があるということである。
しかしながら多重継承を使う方が直感的になる場合もあるとの主張もあり、どちらが正しいとは言えない状況である。
限定多重継承
テンプレート:Main 完全な実装多重継承が問題を起こしやすいのは、継承という仕組みの関係上複数の親クラスが対等に扱われる事が原因である。そこで完全な継承を行なう代わりに能力を限定した実装の引き継ぎを行なう手法もある。これらはモジュール、トレイトなどと呼ばれ、通常の継承とは区別される。継承との違いは次のようなものである。
- インスタンス変数を持たない。つまり特定の構造に依存しない純粋なメソッド定義を行なう。
- メソッドの集合演算が定義できる
- 必要に応じてクラスに付加される。クラスの通常継承時にそれらを引き継ぐ必要はない。
- 通常の継承とは独立の継承構造を持つため構造の把握が行ないやすい。意味のつながりを持たないクラス間で横断的に定義されるメソッドで特に有効である。
これらの特徴から多重継承の問題のうち1,2,4はほぼ解決できる。一般には多重継承を行なう場合も、このような使い方をする事が望ましいと考えられている。
UMLにおける継承
UMLのクラス図においては、BがAを継承する場合、AとBの間には汎化の関係があるという。同時に、AはBを汎化したクラスであるといえる。逆に、BはAを特化したクラスであるともいえる。視覚的な記述方法としては、AとBを線で結び、A側の線の端に白抜きの三角を描くことで表現する。