「継承 (プログラミング)」の版間の差分

出典: フリー百科事典『ウィキペディア(Wikipedia)』
削除された内容 追加された内容
Sycgln (会話 | 投稿記録)
Sycgln (会話 | 投稿記録)
17行目: 17行目:
継承のもう一つの用法は[[派生型|サブタイピング]]であり、特に2000年代の[[オブジェクト指向プログラミング|オブジェクト指向]]では[[SOLID|SOLID原則]]の重視に伴なってこちらがメインになっている。手続き型や関数型でもこれは用いられるが、本節ではオブジェクト指向に限って説明する。サブタイピングの用法は、幅的(width)な拡張化(extension)タイプと、深さ的(depth)な詳細化(refinement)タイプに二分されるが、オブジェクト指向では後者の深さ的に相当する機能(メソッド)の詳細化に特化した{{仮リンク|振る舞いサブタイピング|en|Behavioral subtyping}}がメインに扱われている。2000年代のオブジェクト指向の継承は、振る舞いサブタイピングの実現手段になっていると考えてよい。その言語実装は、[[仮想関数テーブル|仮想関数]]、[[オーバーライド|メソッドオーバーライド]]、[[抽象クラス]]、[[インタフェース (抽象型)|インターフェース]]、[[多重ディスパッチ|多重ディスパッチ関数]]といった言語機能でなされており、これらは{{仮リンク|動的ディスパッチ|en|Dynamic dispatch}}と言われる概念モデルの表現体である。そこではインターフェース(純粋抽象クラス)の継承を指しての実装継承(implementation inheritance)という言葉も登場している。
継承のもう一つの用法は[[派生型|サブタイピング]]であり、特に2000年代の[[オブジェクト指向プログラミング|オブジェクト指向]]では[[SOLID|SOLID原則]]の重視に伴なってこちらがメインになっている。手続き型や関数型でもこれは用いられるが、本節ではオブジェクト指向に限って説明する。サブタイピングの用法は、幅的(width)な拡張化(extension)タイプと、深さ的(depth)な詳細化(refinement)タイプに二分されるが、オブジェクト指向では後者の深さ的に相当する機能(メソッド)の詳細化に特化した{{仮リンク|振る舞いサブタイピング|en|Behavioral subtyping}}がメインに扱われている。2000年代のオブジェクト指向の継承は、振る舞いサブタイピングの実現手段になっていると考えてよい。その言語実装は、[[仮想関数テーブル|仮想関数]]、[[オーバーライド|メソッドオーバーライド]]、[[抽象クラス]]、[[インタフェース (抽象型)|インターフェース]]、[[多重ディスパッチ|多重ディスパッチ関数]]といった言語機能でなされており、これらは{{仮リンク|動的ディスパッチ|en|Dynamic dispatch}}と言われる概念モデルの表現体である。そこではインターフェース(純粋抽象クラス)の継承を指しての実装継承(implementation inheritance)という言葉も登場している。


サブタイピングは継承間クラスの[[Is-a関係]]を保証する用法であり、[[SOLID|SOLID原則]]の[[リスコフの置換原則]]で唱えられている継承間クラスのIs-a関係の順守とは、即ち継承をサブタイピング用法に限定することを唱えているのと同義である。{{仮リンク|振る舞いサブタイピング|en|Behavioral subtyping}}は[[SOLID|SOLID原則]]の[[開放/閉鎖原則|開放閉鎖の原則]]も履行している。{{仮リンク|振る舞いサブタイピング|en|Behavioral subtyping}}の使用例を以下に示す。ここではスーパークラスで定義した仮想関数(仮想メソッド)を、サブクラスで[[オーバーライド]]することで実行時多態性を表現している。
サブタイピングは継承間クラスの[[Is-a関係]]を保証する用法であり、[[SOLID|SOLID原則]]の[[リスコフの置換原則]]で唱えられている継承間クラスのIs-a関係の順守とは、即ち継承をサブタイピング用法に限定することを唱えているのと同義である。{{仮リンク|振る舞いサブタイピング|en|Behavioral subtyping}}は[[SOLID|SOLID原則]]の[[開放/閉鎖原則|開放閉鎖の原則]]も履行している。{{仮リンク|振る舞いサブタイピング|en|Behavioral subtyping}}のコーディング例を以下に示す。ここではスーパークラスで定義した仮想関数(仮想メソッド)を、サブクラスで[[オーバーライド]]することで実行時[[多態性]]を表現している。


<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
44行目: 44行目:
</syntaxhighlight>
</syntaxhighlight>


== 多重継承と仮想継承 ==
== 多重継承 ==
[[多重継承]]はほぼオブジェクト指向で用いられるものなので、本節でもオブジェクト指向に限って説明する。複数のスーパークラスを持たせることを指して多重継承という。単一継承と異なり、多重継承では、スーパークラス上のフィールドとメソッドの検索が複数方向に分かれるので、最終的にどのフィールドとメソッドが参照されているかの把握が困難になるという欠点がある。特にフィールドの多重継承分散配置は、クラス構造上の大きな問題と見なされており、フィールドほどではないがメソッドの方も同様に見なされたので、これはメソッド順序解決(MRO)問題と呼ばれた。MRO問題を解決するために導入されたのが、[[インタフェース (抽象型)|インターフェース]]や[[トレイト]]の[[ミックスイン]]である。加えて多重継承上のスーパークラスの重複による[[菱形継承問題]]も発生する。[[菱形継承問題]]の解決策としては、[[C++]]/[[Eiffel]]発の[[仮想継承]]、[[Eiffel]]発のリネーミング、[[Python]]発のC3線形化などがある。
複数のクラスから継承することを[[多重継承]]いう。多重継承のバリエーションとして[[仮想継承]]がある。同一のクラスから継承している複数の派生クラスを多重継承して1つのクラスを作る場合に始めの基底クラスの存在をどうするかによって仮想継承と通常の多重継承の2つに分かれる。

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

2021年9月22日 (水) 14:49時点における版

継承(けいしょう、: inheritanceインヘリタンスとは、コンピュータプログラミングで用いられる概念であり、任意のオブジェクトの特性を、他のオブジェクトの特性の基礎にするためのメカニズムと定義されている。基礎にされる継承元は親オブジェクト、その継承先は子オブジェクトと呼ばれて、状態機能定数などが引き継がれるが、構築子解体子は概念的には対象外である。

継承を理解する上で、派生(subtyping)との区別は重要である。派生は継承元と継承先をIs-a関係で連結する概念であるが、継承は必ずしもそうではなく、継承元の特性をただ引き継ぐことに専念している。派生オブジェクトは親の機能を拡張または詳細化することに専念するが、継承オブジェクトは親の状態と機能をただ利用しての全く別種の機能を表現することもあり、この場合はIs-a関係でなくなる。オブジェクト指向で言われるリスコフの置換原則は、継承を派生用法に限定してのIs-a関係順守の必要性を訴えた理論であり、この派生用法は同時にメイヤー開放閉鎖の原則にも則っていた。そこでは、親の抽象機能の子による詳細化を指してのオーバーライドという概念と、抽象機能だけで構成されるインターフェースを継承することを指しての実装(implementation)という概念も誕生している。

継承はオブジェクト指向での使用が最も有名でありその親と子の関係は、クラスベースではスーパークラスサブクラスの関係で導入されている。プロトタイプベースではプロトタイプのクローンがスーパークラスの継承に相当しているが[1]、そこではオーバーライドと実装が成立しないのでリスコフの置換原則違反を招きやすくなっている。手続き型関数型プログラミングでは、構造体レコードモジュールで継承が使用される。

継承と対比される概念にコンポジション(合成)英語版がある。継承の派生用法のIs-a関係に対して、合成はHas-a関係であり、継承の派生用法の上位概念と下位概念に対して、合成は上位集合と部分集合である。ただし派生用法無しの継承では、スーパークラスとサブクラスの関係が事実上の下位集合と上位集合の関係になることがしばしばあるので、それと合成との使い分けは特に差分プログラミングでの論点になっている。

継承の目的

差分プログラミング

継承での差分プログラミングとは、上位モジュールを共通パートにし、下位モジュールを差分パートにして、それぞれのモジュールの共通パートをただの記号にした「共通記号+差分パート」として表現する手法を指している。その共通記号+差分パートは、他の下位モジュールのための共通パートになる。共通ソースコードを記号化して他の差分ソースコードに付け足すという方法によって重複記述を削減できた。オブジェクト指向での上位モジュールはスーパークラス、下位モジュールはサブクラスと呼ばれる。継承の差分プログラミング用法は、クラスに新機能を付け足しての手軽なクラス拡張目的と、クラスの共通部分を括りだして体系化するクラス分類目的の双方に使われた。

差分プログラミングは、継承の最も分かりやすい使い方であり、コードの再利用性を高めるものと見なされて、1990年代までのオブジェクト指向では特に多用されていた。継承が初めて導入されたプログラミング言語「Simula67」でも、継承はシミュレーション観測用オブジェクトの分類体系化がメインであり、仮想手続きによるサブタイピングはそのサポート的な機能だった。しかしそれらが長じると、継承チェーン上で分散配置されたデータ構造とメソッドの把握のしづらさによる弊害の方が目立つようになり、2000年代になると差分プログラミング目的の継承は明確に否定されるようになった。同時にその代替としてのコンポジション(合成)英語版が重視されるようになっている。

サブタイピング

継承のもう一つの用法はサブタイピングであり、特に2000年代のオブジェクト指向ではSOLID原則の重視に伴なってこちらがメインになっている。手続き型や関数型でもこれは用いられるが、本節ではオブジェクト指向に限って説明する。サブタイピングの用法は、幅的(width)な拡張化(extension)タイプと、深さ的(depth)な詳細化(refinement)タイプに二分されるが、オブジェクト指向では後者の深さ的に相当する機能(メソッド)の詳細化に特化した振る舞いサブタイピング英語版がメインに扱われている。2000年代のオブジェクト指向の継承は、振る舞いサブタイピングの実現手段になっていると考えてよい。その言語実装は、仮想関数メソッドオーバーライド抽象クラスインターフェース多重ディスパッチ関数といった言語機能でなされており、これらは動的ディスパッチ英語版と言われる概念モデルの表現体である。そこではインターフェース(純粋抽象クラス)の継承を指しての実装継承(implementation inheritance)という言葉も登場している。

サブタイピングは継承間クラスのIs-a関係を保証する用法であり、SOLID原則リスコフの置換原則で唱えられている継承間クラスのIs-a関係の順守とは、即ち継承をサブタイピング用法に限定することを唱えているのと同義である。振る舞いサブタイピング英語版SOLID原則開放閉鎖の原則も履行している。振る舞いサブタイピング英語版のコーディング例を以下に示す。ここではスーパークラスで定義した仮想関数(仮想メソッド)を、サブクラスでオーバーライドすることで実行時多態性を表現している。

#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の基底が共有されている状態である。

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

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

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

ミックスイン

多重継承の亜種にミックスインがある。ミックスインは多重継承で生じる数々の問題を解決するために編み出された手法である。完全な実装多重継承が問題を起こしやすいのは、継承という仕組みの関係上複数の親クラスが対等に扱われる事が原因である。そこで多重継承を行なう代わりに、状態主体のクラスを単一継承にして、振る舞い主体のクラスを多重継承にするという方法論が考案された。振る舞い主体のクラスは、主にトレイトと呼ばれているが、Rubyではモジュールと呼ばれている。トレイトは独立メソッドの複合体である。このトレイトを多重継承(単一でもよい)することを指してミックスインと呼ばれる。ミックスインと継承の違いは次のようなものである。

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

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

UMLにおける継承

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

なお、純粋抽象クラスであるインターフェースとクラスの関係は、実現(realization)と実装(implementation)で表わされている。クラスから見たインターフェースは実現、クラスがインターフェースを継承することは実装と呼ばれる。

脚注

関連項目