継承
継承とは、既存のより一般的な基底型に基づいて、より特殊な型を定義することだ。特殊型は基底型のメンバーを取得するため、その結果、基底型の代わりに使用することができる。
継承は、構造体ではなくクラスで使用できる。別のクラスを継承するクラスはサブクラスと呼ばれ、継承されるクラスはスーパークラス、またはベースクラスと呼ばれる。
Dには2種類の継承がある。この章では実装継承について説明し、インターフェース継承については後の章で説明する。
サブクラスを定義する際は、コロン記号の後にスーパークラスを指定する:
この例を見るために、時計を表す次のクラスがすでに存在すると仮定しよう。
このクラスのメンバーは、構築時に特別な値を必要としないため、コンストラクタは存在しない。その代わりに、メンバーはadjust()
メンバー関数によって設定される。
注釈:時刻文字列は、toString()
関数で生成した方がより便利だ。これは、後でoverride
キーワードの説明で追加する。
出力:
20:30:00
この機能だけなら、Clock
は構造体でもかまわないし、プログラムの必要に応じてそれで十分だろう。
ただし、クラスとして定義することで、Clock
から継承することが可能になる。
継承の例を見るために、Clock
のすべての機能を含むだけでなく、アラームを設定する方法も提供するAlarmClock
を考えてみよう。まず、Clock
を考慮せずにこの型を定義しよう。そうした場合、Clock
と同じ3つのメンバーと、それらを調整する同じadjust()
関数を含める必要がある。AlarmClock
には、追加機能のための他のメンバーも必要になる。
Clock
に正確に存在するメンバーはハイライト表示されている。ご覧の通り、Clock
とAlarmClock
を別々に定義すると、コードの重複が発生する。
このような場合、継承が役立つ。AlarmClock
をClock
から継承することで、新しいクラスが簡素化され、コードの重複が削減される:
AlarmClock
の新しい定義は、以前の定義と等価だ。新しい定義のハイライトされた部分は、古い定義のハイライトされた部分に対応している。
AlarmClock
はClock
のメンバを継承しているため、Clock
と同じように使用できる:
スーパークラスから継承されたメンバーは、サブクラスのメンバーとしてアクセスできる:
出力:
20:30:00 | ♫07:00 |
注釈:この場合、AlarmClock.toString
関数の方がより便利だ。これについては、後で定義する。
この例で使用している継承は、実装継承だ。
メモリを上下に伸びるリボンと想像すると、AlarmClock
のメンバのメモリ上の配置は次の図のようにイメージできる:
│ . │ │ . │ オブジェクトのアドレス → ├─────────────┤ │(他のデータ) │ │ 時 │ │ 分 │ │ 秒 │ │ alarmHour │ │ alarmMinute │ ├─────────────┤ │ . │ │ . │
上の図は、スーパークラスとサブクラスのメンバーがどのように組み合わされるかを示すためのものだ。メンバーの実際のレイアウトは、使用しているコンパイラの実装の詳細によって異なる。例えば、その他のデータとしてマークされている部分には、通常、その特定のクラスタイプの仮想関数テーブル(vtbl) へのポインタが含まれる。オブジェクトのレイアウトの詳細については、この書籍では扱わない。
警告: "はである"の場合にのみ継承する
実装継承はメンバーを取得することであることがわかった。この種の継承は、サブタイプが"アラーム時計は時計の一種である"という表現のように、スーパークラスの一種と考えることができる場合にのみ検討。
型間の関係には"Is a"だけではない。より一般的な関係は"has a"という関係である。例えば、Clock
クラスにBattery
という概念を追加したいとする。Clock
にBattery
を継承によって追加するのは適切ではない。なぜなら、"時計は電池である"という文は真ではないからだ。
時計は電池ではない。時計には電池が搭載されている。このような包含関係がある場合、包含される型は、それを包含する型のメンバーとして定義する必要がある。
1つのクラスからの継承
クラスは、単一の基底クラス(その基底クラス自身も別の単一のクラスから継承する可能性がある)からのみ継承できる。つまり、Dでは多重継承はサポートされていない。
例えば、SoundEmitter
クラスも存在し、"目覚まし時計は音を発するオブジェクトである"も真であるにもかかわらず、Clock
とSoundEmitter
の両方からAlarmClock
を継承することはできない。
一方、クラスが継承できるインターフェースの数に制限はない。interface
キーワードについては後で説明する。
さらに、継承階層の深さにも制限はない:
上記の継承階層は、より一般的なものからより具体的なものへの関係を定義している:楽器、弦楽器、バイオリン。
階層図
"は、である"という関係にある型は、クラス階層を形成する。
オブジェクト指向プログラミングの慣習に従い、クラス階層はスーパークラスが上部に、サブクラスが下部に配置される。継承関係は、サブクラスからスーパークラスを指す矢印で示される。
例えば、以下は楽器の階層構造だ。
MusicalInstrument ↗ ↖ StringInstrument WindInstrument ↗ ↖ ↗ ↖ Violin Guitar Flute Recorder
スーパークラスのメンバーへのアクセス
super
キーワードを使用すると、スーパークラスから継承されたメンバーを参照できる。
super
キーワードは必ずしも必要ではなく、上記のコードではminute
だけで同じ意味を持つ。super
キーワードは、スーパークラスとサブクラスが同じ名前のメンバーを持つ場合に必要になる。これは、super.reset()
とsuper.toString()
を記述する必要がある場合に後で説明する。
継承ツリー内の複数のクラスが同じ名前のシンボルを定義している場合、継承ツリー内のクラスの固有の名前を使用して、シンボルを区別することができる:
スーパークラスのメンバーの構築
super
キーワードのもう1つの用途は、スーパークラスのコンストラクタを呼び出すことだ。これは、現在のクラスのオーバーロードされたコンストラクタを呼び出す場合と類似している:現在のクラスのコンストラクタを呼び出す場合はthis
、スーパークラスのコンストラクタを呼び出す場合はsuper
となる。
スーパークラスのコンストラクタを明示的に呼び出す必要はない。サブクラスのコンストラクタがsuper
のオーバーロードを明示的に呼び出す場合、その呼び出しによってそのコンストラクタが実行される。そうでない場合、かつスーパークラスにデフォルトコンストラクタが存在する場合、サブクラスの本体に入る前に自動的に実行される。
Clock
およびAlarmClock
クラスには、まだコンストラクタを定義していない。そのため、これらのクラスのメンバーは、それぞれの型の.init
値(int
の場合は0)によって初期化される。
Clock
に次のコンストラクタがあると仮定しよう。
このコンストラクタは、Clock
オブジェクトを構築する際には必ず使用する必要がある:
当然、Clock
型を直接使用するプログラマは、その構文を使用しなければならない。しかし、AlarmClock
オブジェクトを構築する場合、そのClock
部分を個別に構築することはできない。さらに、AlarmClock
のユーザーは、それがClock
から継承していることを知る必要すらない。
AlarmClock
のユーザーは、そのClock
の継承に注意を払う必要なく、AlarmClock
オブジェクトを構築してプログラムで使用するだけでよい。
そのため、スーパークラスの部分を構築するのはサブクラスの責任だ。サブクラスは、super()
の構文を使用してスーパークラスのコンストラクタを呼び出す:
AlarmClock
のコンストラクタは、自身のメンバーとスーパークラスのメンバーの両方の引数を受け取る。その後、それらの引数のうちの一部を使用して、スーパークラスの部分を構築する。
メンバー関数の定義のオーバーライド
継承の利点の1つは、サブクラスでスーパークラスのメンバー関数を再定義できることだ。これはオーバーライドと呼ばれる。スーパークラスのメンバー関数の既存の定義は、override
キーワードによってサブクラスでオーバーライドされる。
オーバーライド可能な関数は、仮想関数と呼ばれる。仮想関数は、仮想関数ポインタテーブル(vtbl) およびvtbl ポインタによってコンパイラによって実装される。このメカニズムの詳細については、この書籍では扱わない。ただし、仮想関数の呼び出しは通常の関数の呼び出しよりもコストが高いことを、すべてのシステムプログラマは知っておく必要がある。Dの非プライベートなclass
メンバー関数は、デフォルトで仮想関数である。そのため、スーパークラスの関数をまったくオーバーライドする必要がない場合は、仮想関数にならないようにfinal
として定義する必要がある。final
キーワードについては、後でインターフェースの章で説明する。
Clock
に、そのメンバーをすべて0にリセットするためのメンバー関数があると仮定しよう。
この関数はAlarmClock
によって継承され、AlarmClock
オブジェクトで呼び出すことができる。
ただし、Clock.reset
はAlarmClock
のメンバについて知らないため、自身のメンバのみをリセットできる。そのため、サブクラスのメンバもリセットするには、reset()
をオーバーライドする必要がある:
サブクラスは、自身のメンバーのみをリセットし、残りのタスクはsuper.reset()
呼び出しによってClock
にディスパッチする。reset()
とだけ記述しても、AlarmClock
自体のreset()
関数が呼び出されてしまうため、機能しないことに注意。reset()
を自身の中から呼び出すと、無限再帰が発生する。
toString()
の定義をここまで遅らせたのは、クラスではoverride
キーワードで定義しなければならないためだ。次の章で見るように、すべてのクラスはObject
というスーパークラスから自動的に継承され、Object
はすでにtoString()
メンバー関数を定義している。
そのため、クラスのtoString()
メンバー関数は、override
キーワードを使用して定義する必要がある。
AlarmClock
は、super.toString()
の呼び出しによって、一部のタスクをClock
にディスパッチしていることに注意。
これらの2つのtoString()
のオーバーライドにより、AlarmClock
オブジェクトを文字列に変換できる:
出力:
10:15:00 | ♫06:45 |
スーパークラスのかわりにサブクラスを使用する
スーパークラスはより一般的であり、サブクラスはより特殊であるため、サブクラスのオブジェクトは、スーパークラスの型のオブジェクトが必要な場所で使用することができる。これは多態性と呼ばれる。
一般的な型と特殊な型の概念は、"この型はあの型である"という文で見ることができる。"目覚まし時計は時計である"、"学生は人である"、"猫は動物である"などである。したがって、目覚まし時計は時計が必要な場所で使用でき、学生は人が必要な場所で使用でき、猫は動物が必要な場所で使用できる。
サブクラスのオブジェクトがスーパークラスのオブジェクトとして使用されている場合、そのオブジェクトは独自の特殊型を失うことはない。これは、実際の例と似ている。目覚まし時計を単なる時計として使用しても、それが目覚まし時計であるという事実は変わらない。
ある関数が、Clock
オブジェクトをパラメータとして受け取り、その実行中にそのオブジェクトをリセットするとする。
多態性により、AlarmClock
をそのような関数に送ることが可能になる。
これは"アラーム時計は時計である"という関係に一致している。その結果、deskClock
オブジェクトのメンバーがリセットされる:
前 | 10:15:00 | ♫06:45 |
---|---|---|
後 | 00:00:00 | ♫00:00 |
ここで重要な点は、Clock
のメンバーだけでなく、AlarmClock
のメンバーもリセットされていることである。
use()
はClock
オブジェクトに対してreset()
を呼び出すが、実際のオブジェクトはAlarmClock
であるため、呼び出される関数はAlarmClock.reset
になる。上記の定義に従って、AlarmClock.reset
はClock
とAlarmClock
の両方のメンバーをリセットする。
つまり、use()
はオブジェクトをClock
として使用しているが、実際のオブジェクトは独自の特別な動作をする継承型である可能性がある。
Clock
階層に別のクラスを追加しよう。この型のreset()
関数は、そのメンバーをランダムな値に設定する。
BrokenClock
のオブジェクトがuse()
に送信されると、BrokenClock
の特別なreset()
関数が呼び出される。ここでも、Clock
として渡されるものの、実際のオブジェクトは依然としてBrokenClock
である。
出力には、BrokenClock
をリセットした結果、ランダムな時間値が表示される。
22:46:37
継承は推移的
ポリモーフィズムは2つのクラスに限定されない。サブクラスのサブクラスも、階層内の任意のスーパークラスの代わりに使用できる。
MusicalInstrument
の階層を考えてみよう:
上記の継承関係は、以下の関係を構築する:"string instrumentはmusical instrumentである"および"violinはstring instrumentである"。したがって、"violinはmusical instrumentである"も真である。したがって、Violin
オブジェクトはMusicalInstrument
の代わりに使用できる。
以下のサポートコードがすべて定義されていると仮定する:
playInTune()
はMusicalInstrument
を期待しているが、関係"violinはmusical instrumentである"のため、Violin
で呼び出されている。
継承は必要なだけ深く行うことができる。
抽象メンバー関数および抽象クラス
クラスが定義を提供できないにもかかわらず、クラスインターフェースに当然出現するメンバー関数がある場合がある。メンバー関数の具体的な定義がない場合、その関数は抽象メンバー関数だ。少なくとも1つの抽象メンバー関数を持つクラスは、抽象クラスである。
例えば、階層構造の親クラスChessPiece
には、指定された移動がそのチェスの駒に対して有効であるかどうかを判断するメンバー関数isValid()
があるかもしれない。移動の有効性はチェスの駒の実際の型によって決まるため、親クラスChessPiece
は、この判断を自分で行うことはできない。有効な移動は、Pawn
、King
などのサブクラスでしか知ることができない。
abstract
キーワードは、継承クラスがこのようなメソッドを自身で実装しなければならないことを指定する:
抽象クラスのオブジェクトを構築することはできない:
サブクラスは、クラスを非抽象クラスにして構築可能にするために、すべての抽象関数をオーバーライドして実装する必要がある。
これで、Pawn
のオブジェクトを構築できるようになった:
抽象関数は独自の実装を持つことができるが、その関数の実装はサブクラスで提供する必要があることに注意。例えば、ChessPiece
の実装は、独自の便利なチェック機能を提供することができる。
isValid()
が実装されていても、ChessPiece
クラスは依然として抽象クラスだが、Pawn
クラスは非抽象クラスであり、インスタンス化可能である。
例
鉄道車両を表すクラス階層を考えてみよう:
RailwayVehicle / | \ Locomotive Train RailwayCar { load()?, unload()? } / \ PassengerCar FreightCar
RailwayCar
がabstract
として宣言する関数は、疑問符で示している。
私の目的は、クラス階層を示し、その設計上の決定事項の一部を指摘することだけなので、これらのクラスを完全に実装しない。実際の処理を行う代わりに、単にメッセージを表示するだけにする。
上記の階層構造で最も一般的なクラスはRailwayVehicle
である。このプログラムでは、自身を移動する方法のみを知っている:
RailwayVehicle
から継承するクラスはLocomotive
で、まだ特別なメンバーは持っていない:
後で演習の中で、Locomotive
に特別なmakeSound()
メンバー関数を追加する。
RailwayCar
もRailwayVehicle
である。ただし、階層構造がさまざまなタイプの鉄道車両をサポートしている場合、積み込みや積み下ろしなどの特定の動作は、その車両の正確なタイプに応じて実行する必要がある。そのため、RailwayCar
では、次の2つの関数を抽象関数として宣言するしかない。
乗用車の乗降は、車のドアを開けるだけの簡単な作業だが、貨物車の乗降には、ポーターやウインチが必要になる場合がある。以下のサブクラスは、RailwayCar
の抽象関数の定義を提供している。
抽象クラスであることは、プログラムでRailwayCar
を使用することを妨げるものではない。RailwayCar
のオブジェクトは構築できないが、RailwayCar
はインターフェースとして使用できる。サブクラスは"乗客用車両は鉄道車両である"と"貨物用車両は鉄道車両である"という2つの関係を定義しているため、PassengerCar
とFreightCar
のオブジェクトはRailwayCar
の代わりに使うことができる。これは、以下のTrain
クラスで確認できる。
列車を表すクラスは、機関車と鉄道車両の配列で構成される:
重要な点を繰り返しておこう。Locomotive
とRailwayCar
はどちらもRailwayVehicle
から継承しているが、Train
をどちらからも継承することは正しくない。継承は"である"という関係をモデル化しており、列車は機関車でも客車でもない。列車はそれらで構成されている。
すべての列車に機関車が必要であると仮定すると、Train
コンストラクタは、有効なLocomotive
オブジェクトを受け取ることを保証する必要がある。同様に、鉄道車両がオプションである場合は、メンバー関数によって追加することができる。
addCar()
は、RailwayCar
オブジェクトも検証できることに注意しよう。ここでは、その検証は無視する。
列車の出発と到着もサポートする必要があると考えられる:
以下のmain()
は、RailwayVehicle
階層を利用している:
Train
クラスは、2つの別々のインターフェースによって提供される関数によって使用されている。
advance()
関数が呼び出されると、その関数はRailwayVehicle
によって宣言されているため、Train
オブジェクトがRailwayVehicle
として使用される。departStation()
およびarriveStation()
関数が呼び出されると、train
オブジェクトがTrain
として使用される。これは、これらの関数がTrain
によって宣言されているためだ。
矢印は、load()
およびunload()
関数が、RailwayCar
の実際の型に従って動作することを示している。
乗客が乗車している ←
木箱が積み込まれている ←
アンカラ駅を出発
車両は500キロメートル進んでいる
ハイダルパシャ駅に到着
乗客が降車している ←
木箱が積み降ろされている ←
要約
- 継承は"はである"関係に使用される。
- すべてのクラスは、最大1つの
class
から継承できる。 super
には2つの用途がある:スーパークラスのコンストラクタを呼び出すことと、スーパークラスのメンバーにアクセスすること。override
は、サブクラス用にスーパークラスのメンバー関数を特別に再定義するためのものだ。abstract
メンバー関数をオーバーライドする必要がある。
演習
RailwayVehicle
を変更しよう。前進した距離を報告するだけでなく、音も鳴らすようにしよう。出力を短くするために、100 キロメートルごとに音を鳴らすようにしよう。ただし、車両によって音が異なる可能性があるため、
makeSound()
はRailwayVehicle
で定義できない:Locomotiveには"チューチュー"
RailwayCarには"カチャカチャ"
注釈:
Train.makeSound
は次の演習で残しておいてほしい。オーバーライドする必要があるため、
makeSound()
はスーパークラスでabstract
として宣言する必要がある。サブクラスで
makeSound()
を実装し、次のmain()
でコードを試してみてみよう。プログラムが以下の出力を生成するようにしよう。
車両は100キロメートル進んでいる カチャカチャ 車両は200キロメートル進んでいる カチャカチャ カチャカチャ 車両は300キロメートル進んでいる チューチュー チューチュー チューチュー
PassengerCar
とFreightCar
の音が異なる必要はない。これらは、RailwayCar
と同じ実装を共有することができる。makeSound()
をTrain
で実装する方法を考えてみよう。1つのアイデアは、Train.makeSound
が、Train
のメンバーの音で構成されるstring
を返すことだ。