継承

継承とは、既存のより一般的な基底型に基づいて、より特殊な型を定義することだ。特殊型は基底型のメンバーを取得するため、その結果、基底型の代わりに使用することができる。

継承は、構造体ではなくクラスで使用できる。別のクラスを継承するクラスはサブクラスと呼ばれ、継承されるクラスはスーパークラス、またはベースクラスと呼ばれる。

Dには2種類の継承がある。この章では実装継承について説明し、インターフェース継承については後の章で説明する。

サブクラスを定義する際は、コロン記号の後にスーパークラスを指定する:

class SubClass : SuperClass {
    // ...
}
D

この例を見るために、時計を表す次のクラスがすでに存在すると仮定しよう。

class Clock {
    int hour;
    int minute;
    int second;

    void adjust(int hour, int minute, int second = 0) {
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }
}
D

このクラスのメンバーは、構築時に特別な値を必要としないため、コンストラクタは存在しない。その代わりに、メンバーはadjust()メンバー関数によって設定される。

auto deskClock = new Clock;
deskClock.adjust(20, 30);
writefln(
    "%02s:%02s:%02s",
    deskClock.hour, deskClock.minute, deskClock.second);
D

注釈:時刻文字列は、toString()関数で生成した方がより便利だ。これは、後でoverrideキーワードの説明で追加する。

出力:

20:30:00

この機能だけなら、Clockは構造体でもかまわないし、プログラムの必要に応じてそれで十分だろう。

ただし、クラスとして定義することで、Clockから継承することが可能になる。

継承の例を見るために、Clockのすべての機能を含むだけでなく、アラームを設定する方法も提供するAlarmClockを考えてみよう。まず、Clockを考慮せずにこの型を定義しよう。そうした場合、Clockと同じ3つのメンバーと、それらを調整する同じadjust()関数を含める必要がある。AlarmClockには、追加機能のための他のメンバーも必要になる。

class AlarmClock {
    int hour;
    int minute;
    int second;
    int alarmHour;
    int alarmMinute;

    void adjust(int hour, int minute, int second = 0) {
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }

    void adjustAlarm(int hour, int minute) {
        alarmHour = hour;
        alarmMinute = minute;
    }
}
D

Clockに正確に存在するメンバーはハイライト表示されている。ご覧の通り、ClockAlarmClockを別々に定義すると、コードの重複が発生する。

このような場合、継承が役立つ。AlarmClockClockから継承することで、新しいクラスが簡素化され、コードの重複が削減される:

class AlarmClock : Clock {
    int alarmHour;
    int alarmMinute;

    void adjustAlarm(int hour, int minute) {
        alarmHour = hour;
        alarmMinute = minute;
    }
}
D

AlarmClockの新しい定義は、以前の定義と等価だ。新しい定義のハイライトされた部分は、古い定義のハイライトされた部分に対応している。

AlarmClockClockのメンバを継承しているため、Clockと同じように使用できる:

auto bedSideClock = new AlarmClock;
bedSideClock.adjust(20, 30);
bedSideClock.adjustAlarm(7, 0);
D

スーパークラスから継承されたメンバーは、サブクラスのメンバーとしてアクセスできる:

writefln("%02s:%02s:%02s ♫%02s:%02s",
         bedSideClock.hour,
         bedSideClock.minute,
         bedSideClock.second,
         bedSideClock.alarmHour,
         bedSideClock.alarmMinute);
D

出力:

20:30:00♫07:00

注釈:この場合、AlarmClock.toString関数の方がより便利だ。これについては、後で定義する。

この例で使用している継承は、実装継承だ。

メモリを上下に伸びるリボンと想像すると、AlarmClockのメンバのメモリ上の配置は次の図のようにイメージできる:

                            │      .      │
                            │      .      │
オブジェクトのアドレス   → ├─────────────┤
                            │(他のデータ) │
                            │ 時        │
                            │ 分      │
                            │ 秒      │
                            │ alarmHour   │
                            │ alarmMinute │
                            ├─────────────┤
                            │      .      │
                            │      .      │

上の図は、スーパークラスとサブクラスのメンバーがどのように組み合わされるかを示すためのものだ。メンバーの実際のレイアウトは、使用しているコンパイラの実装の詳細によって異なる。例えば、その他のデータとしてマークされている部分には、通常、その特定のクラスタイプの仮想関数テーブル(vtbl) へのポインタが含まれる。オブジェクトのレイアウトの詳細については、この書籍では扱わない。

警告: "はである"の場合にのみ継承する

実装継承はメンバーを取得することであることがわかった。この種の継承は、サブタイプが"アラーム時計は時計の一種である"という表現のように、スーパークラスの一種と考えることができる場合にのみ検討。

型間の関係には"Is a"だけではない。より一般的な関係は"has a"という関係である。例えば、ClockクラスにBatteryという概念を追加したいとする。ClockBatteryを継承によって追加するのは適切ではない。なぜなら、"時計は電池である"という文は真ではないからだ。

class Clock : Battery {    // ← 間違ったデザイン
    // ...
}
D

時計は電池ではない。時計には電池が搭載されている。このような包含関係がある場合、包含される型は、それを包含する型のメンバーとして定義する必要がある。

class Clock {
    Battery battery;       // ← 正しいデザイン
    // ...
}
D
1つのクラスからの継承

クラスは、単一の基底クラス(その基底クラス自身も別の単一のクラスから継承する可能性がある)からのみ継承できる。つまり、Dでは多重継承はサポートされていない。

例えば、SoundEmitterクラスも存在し、"目覚まし時計は音を発するオブジェクトである"も真であるにもかかわらず、ClockSoundEmitterの両方からAlarmClockを継承することはできない。

class SoundEmitter {
    // ...
}

class AlarmClock : Clock, SoundEmitter {    // ← コンパイルエラー
    // ...
}
D

一方、クラスが継承できるインターフェースの数に制限はない。interfaceキーワードについては後で説明する。

さらに、継承階層の深さにも制限はない:

class MusicalInstrument {
    // ...
}

class StringInstrument : MusicalInstrument {
    // ...
}

class Violin : StringInstrument {
    // ...
}
D

上記の継承階層は、より一般的なものからより具体的なものへの関係を定義している:楽器、弦楽器、バイオリン。

階層図

"は、である"という関係にある型は、クラス階層を形成する。

オブジェクト指向プログラミングの慣習に従い、クラス階層はスーパークラスが上部に、サブクラスが下部に配置される。継承関係は、サブクラスからスーパークラスを指す矢印で示される。

例えば、以下は楽器の階層構造だ。

             MusicalInstrument
                ↗         ↖
    StringInstrument   WindInstrument
         ↗    ↖            ↗    ↖
     Violin  Guitar    Flute   Recorder
スーパークラスのメンバーへのアクセス

superキーワードを使用すると、スーパークラスから継承されたメンバーを参照できる。

class AlarmClock : Clock {
    // ...

    void foo() {
        super.minute = 10; // 継承された'minute'メンバー
        minute = 10;       // 曖昧さがなければ同じこと
    }
}
D

superキーワードは必ずしも必要ではなく、上記のコードではminuteだけで同じ意味を持つ。superキーワードは、スーパークラスとサブクラスが同じ名前のメンバーを持つ場合に必要になる。これは、super.reset()super.toString()を記述する必要がある場合に後で説明する。

継承ツリー内の複数のクラスが同じ名前のシンボルを定義している場合、継承ツリー内のクラスの固有の名前を使用して、シンボルを区別することができる:

class Device {
    string manufacturer;
}

class Clock : Device {
    string manufacturer;
}

class AlarmClock : Clock {
    // ...

    void foo() {
        Device.manufacturer = "Sunny Horology, Inc.";
        Clock.manufacturer = "Better Watches, Ltd.";
    }
}
D
スーパークラスのメンバーの構築

superキーワードのもう1つの用途は、スーパークラスのコンストラクタを呼び出すことだ。これは、現在のクラスのオーバーロードされたコンストラクタを呼び出す場合と類似している:現在のクラスのコンストラクタを呼び出す場合はthis、スーパークラスのコンストラクタを呼び出す場合はsuperとなる。

スーパークラスのコンストラクタを明示的に呼び出す必要はない。サブクラスのコンストラクタがsuperのオーバーロードを明示的に呼び出す場合、その呼び出しによってそのコンストラクタが実行される。そうでない場合、かつスーパークラスにデフォルトコンストラクタが存在する場合、サブクラスの本体に入る前に自動的に実行される。

ClockおよびAlarmClockクラスには、まだコンストラクタを定義していない。そのため、これらのクラスのメンバーは、それぞれの型の.init値(intの場合は0)によって初期化される。

Clockに次のコンストラクタがあると仮定しよう。

class Clock {
    this(int hour, int minute, int second) {
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }

    // ...
}
D

このコンストラクタは、Clockオブジェクトを構築する際には必ず使用する必要がある:

auto clock = new Clock(17, 15, 0);
D

当然、Clock型を直接使用するプログラマは、その構文を使用しなければならない。しかし、AlarmClockオブジェクトを構築する場合、そのClock部分を個別に構築することはできない。さらに、AlarmClockのユーザーは、それがClockから継承していることを知る必要すらない。

AlarmClockのユーザーは、そのClockの継承に注意を払う必要なく、AlarmClockオブジェクトを構築してプログラムで使用するだけでよい。

auto bedSideClock = new AlarmClock(/* ... */);
// ... AlarmClockとして使う ...
D

そのため、スーパークラスの部分を構築するのはサブクラスの責任だ。サブクラスは、super()の構文を使用してスーパークラスのコンストラクタを呼び出す:

class AlarmClock : Clock {
    this(int hour, int minute, int second,  // Clockのメンバー用
         int alarmHour, int alarmMinute) {  // AlarmClockのメンバー用
        super(hour, minute, second);
        this.alarmHour = alarmHour;
        this.alarmMinute = alarmMinute;
    }

    // ...
}
D

AlarmClockのコンストラクタは、自身のメンバーとスーパークラスのメンバーの両方の引数を受け取る。その後、それらの引数のうちの一部を使用して、スーパークラスの部分を構築する。

メンバー関数の定義のオーバーライド

継承の利点の1つは、サブクラスでスーパークラスのメンバー関数を再定義できることだ。これはオーバーライドと呼ばれる。スーパークラスのメンバー関数の既存の定義は、overrideキーワードによってサブクラスでオーバーライドされる。

オーバーライド可能な関数は、仮想関数と呼ばれる。仮想関数は、仮想関数ポインタテーブル(vtbl) およびvtbl ポインタによってコンパイラによって実装される。このメカニズムの詳細については、この書籍では扱わない。ただし、仮想関数の呼び出しは通常の関数の呼び出しよりもコストが高いことを、すべてのシステムプログラマは知っておく必要がある。Dの非プライベートなclassメンバー関数は、デフォルトで仮想関数である。そのため、スーパークラスの関数をまったくオーバーライドする必要がない場合は、仮想関数にならないようにfinalとして定義する必要がある。finalキーワードについては、後でインターフェースの章で説明する。

Clockに、そのメンバーをすべて0にリセットするためのメンバー関数があると仮定しよう。

class Clock {
    void reset() {
        hour = 0;
        minute = 0;
        second = 0;
    }

    // ...
}
D

この関数はAlarmClockによって継承され、AlarmClockオブジェクトで呼び出すことができる。

auto bedSideClock = new AlarmClock(20, 30, 0, 7, 0);
// ...
bedSideClock.reset();
D

ただし、Clock.resetAlarmClockのメンバについて知らないため、自身のメンバのみをリセットできる。そのため、サブクラスのメンバもリセットするには、reset()をオーバーライドする必要がある:

class AlarmClock : Clock {
    override void reset() {
        super.reset();
        alarmHour = 0;
        alarmMinute = 0;
    }

    // ...
}
D

サブクラスは、自身のメンバーのみをリセットし、残りのタスクはsuper.reset()呼び出しによってClockにディスパッチする。reset()とだけ記述しても、AlarmClock自体のreset()関数が呼び出されてしまうため、機能しないことに注意。reset()を自身の中から呼び出すと、無限再帰が発生する。

toString()の定義をここまで遅らせたのは、クラスではoverrideキーワードで定義しなければならないためだ。次の章で見るように、すべてのクラスはObjectというスーパークラスから自動的に継承され、ObjectはすでにtoString()メンバー関数を定義している。

そのため、クラスのtoString()メンバー関数は、overrideキーワードを使用して定義する必要がある。

import std.string;

class Clock {
    override string toString() const {
        return format("%02s:%02s:%02s", hour, minute, second);
    }

    // ...
}

class AlarmClock : Clock {
    override string toString() const {
        return format("%s ♫%02s:%02s", super.toString(),
                      alarmHour, alarmMinute);
    }

    // ...
}
D

AlarmClockは、super.toString()の呼び出しによって、一部のタスクをClockにディスパッチしていることに注意。

これらの2つのtoString()のオーバーライドにより、AlarmClockオブジェクトを文字列に変換できる:

void main() {
    auto deskClock = new AlarmClock(10, 15, 0, 6, 45);
    writeln(deskClock);
}
D

出力:

10:15:00♫06:45
スーパークラスのかわりにサブクラスを使用する

スーパークラスはより一般的であり、サブクラスはより特殊であるため、サブクラスのオブジェクトは、スーパークラスの型のオブジェクトが必要な場所で使用することができる。これは多態性と呼ばれる。

一般的な型と特殊な型の概念は、"この型はあの型である"という文で見ることができる。"目覚まし時計は時計である"、"学生は人である"、"猫は動物である"などである。したがって、目覚まし時計は時計が必要な場所で使用でき、学生は人が必要な場所で使用でき、猫は動物が必要な場所で使用できる。

サブクラスのオブジェクトがスーパークラスのオブジェクトとして使用されている場合、そのオブジェクトは独自の特殊型を失うことはない。これは、実際の例と似ている。目覚まし時計を単なる時計として使用しても、それが目覚まし時計であるという事実は変わらない。

ある関数が、Clockオブジェクトをパラメータとして受け取り、その実行中にそのオブジェクトをリセットするとする。

void use(Clock clock) {
    // ...
    clock.reset();
    // ...
}
D

多態性により、AlarmClockをそのような関数に送ることが可能になる。

auto deskClock = new AlarmClock(10, 15, 0, 6, 45);
writeln("Before: ", deskClock);
use(deskClock);
writeln("After : ", deskClock);
D

これは"アラーム時計は時計である"という関係に一致している。その結果、deskClockオブジェクトのメンバーがリセットされる:

10:15:00♫06:45
00:00:0000:00

ここで重要な点は、Clockのメンバーだけでなく、AlarmClockのメンバーもリセットされていることである。

use()Clockオブジェクトに対してreset()を呼び出すが、実際のオブジェクトはAlarmClockであるため、呼び出される関数はAlarmClock.resetになる。上記の定義に従って、AlarmClock.resetClockAlarmClockの両方のメンバーをリセットする。

つまり、use()はオブジェクトをClockとして使用しているが、実際のオブジェクトは独自の特別な動作をする継承型である可能性がある。

Clock階層に別のクラスを追加しよう。この型のreset()関数は、そのメンバーをランダムな値に設定する。

import std.random;

class BrokenClock : Clock {
    this() {
        super(0, 0, 0);
    }

    override void reset() {
        hour = uniform(0, 24);
        minute = uniform(0, 60);
        second = uniform(0, 60);
    }
}
D

BrokenClockのオブジェクトがuse()に送信されると、BrokenClockの特別なreset()関数が呼び出される。ここでも、Clockとして渡されるものの、実際のオブジェクトは依然としてBrokenClockである。

auto shelfClock = new BrokenClock;
use(shelfClock);
writeln(shelfClock);
D

出力には、BrokenClockをリセットした結果、ランダムな時間値が表示される。

22:46:37
継承は推移的

ポリモーフィズムは2つのクラスに限定されない。サブクラスのサブクラスも、階層内の任意のスーパークラスの代わりに使用できる。

MusicalInstrumentの階層を考えてみよう:

class MusicalInstrument {
    // ...
}

class StringInstrument : MusicalInstrument {
    // ...
}

class Violin : StringInstrument {
    // ...
}
D

上記の継承関係は、以下の関係を構築する:"string instrumentはmusical instrumentである"および"violinはstring instrumentである"。したがって、"violinはmusical instrumentである"も真である。したがって、ViolinオブジェクトはMusicalInstrumentの代わりに使用できる。

以下のサポートコードがすべて定義されていると仮定する:

void playInTune(MusicalInstrument instrument,
                MusicalPiece piece) {
    instrument.tune();
    instrument.play(piece);
}

// ...

auto myViolin = new Violin;
playInTune(myViolin, improvisation);
D

playInTune()MusicalInstrumentを期待しているが、関係"violinはmusical instrumentである"のため、Violinで呼び出されている。

継承は必要なだけ深く行うことができる。

抽象メンバー関数および抽象クラス

クラスが定義を提供できないにもかかわらず、クラスインターフェースに当然出現するメンバー関数がある場合がある。メンバー関数の具体的な定義がない場合、その関数は抽象メンバー関数だ。少なくとも1つの抽象メンバー関数を持つクラスは、抽象クラスである。

例えば、階層構造の親クラスChessPieceには、指定された移動がそのチェスの駒に対して有効であるかどうかを判断するメンバー関数isValid()があるかもしれない。移動の有効性はチェスの駒の実際の型によって決まるため、親クラスChessPieceは、この判断を自分で行うことはできない。有効な移動は、PawnKingなどのサブクラスでしか知ることができない。

abstractキーワードは、継承クラスがこのようなメソッドを自身で実装しなければならないことを指定する:

class ChessPiece {
    abstract bool isValid(Square from, Square to);
}
D

抽象クラスのオブジェクトを構築することはできない:

auto piece = new ChessPiece;    // ← コンパイルエラー
D

サブクラスは、クラスを非抽象クラスにして構築可能にするために、すべての抽象関数をオーバーライドして実装する必要がある。

class Pawn : ChessPiece {
    override bool isValid(Square from, Square to) {
        // ... pawnに対するisValidの実装 ...
        return decision;
    }
}
D

これで、Pawnのオブジェクトを構築できるようになった:

auto piece = new Pawn;             // コンパイルできる
D

抽象関数は独自の実装を持つことができるが、その関数の実装はサブクラスで提供する必要があることに注意。例えば、ChessPieceの実装は、独自の便利なチェック機能を提供することができる。

class ChessPiece {
    abstract bool isValid(Square from, Square to) {
        // 'to'の位置は'from'の位置と
        // 異なる必要がある
        return from != to;
    }
}

class Pawn : ChessPiece {
    override bool isValid(Square from, Square to) {
        // まず、それがChessPieceにとって有効な移動であるかどうかを確認する
        if (!super.isValid(from, to)) {
            return false;
        }

        // ... 次に、それがPawnにとって有効であるかどうかを確認する ...

        return decision;
    }
}
D

isValid()が実装されていても、ChessPieceクラスは依然として抽象クラスだが、Pawnクラスは非抽象クラスであり、インスタンス化可能である。

鉄道車両を表すクラス階層を考えてみよう:

           RailwayVehicle
          /      |       \
  Locomotive   Train   RailwayCar { load()?, unload()? }
                          /   \
               PassengerCar   FreightCar

RailwayCarabstractとして宣言する関数は、疑問符で示している。

私の目的は、クラス階層を示し、その設計上の決定事項の一部を指摘することだけなので、これらのクラスを完全に実装しない。実際の処理を行う代わりに、単にメッセージを表示するだけにする。

上記の階層構造で最も一般的なクラスはRailwayVehicleである。このプログラムでは、自身を移動する方法のみを知っている:

class RailwayVehicle {
    void advance(size_t kilometers) {
        writefln("The vehicle is advancing %s kilometers",
                 kilometers);
    }
}
D
inheritance.2

RailwayVehicleから継承するクラスはLocomotiveで、まだ特別なメンバーは持っていない:

class Locomotive : RailwayVehicle {
}
D
inheritance.2

後で演習の中で、Locomotiveに特別なmakeSound()メンバー関数を追加する。

RailwayCarRailwayVehicleである。ただし、階層構造がさまざまなタイプの鉄道車両をサポートしている場合、積み込みや積み下ろしなどの特定の動作は、その車両の正確なタイプに応じて実行する必要がある。そのため、RailwayCarでは、次の2つの関数を抽象関数として宣言するしかない。

class RailwayCar : RailwayVehicle {
    abstract void load();
    abstract void unload();
}
D
inheritance.2

乗用車の乗降は、車のドアを開けるだけの簡単な作業だが、貨物車の乗降には、ポーターやウインチが必要になる場合がある。以下のサブクラスは、RailwayCarの抽象関数の定義を提供している。

class PassengerCar : RailwayCar {
    override void load() {
        writeln("The passengers are getting on");
    }

    override void unload() {
        writeln("The passengers are getting off");
    }
}

class FreightCar : RailwayCar {
    override void load() {
        writeln("The crates are being loaded");
    }

    override void unload() {
        writeln("The crates are being unloaded");
    }
}
D
inheritance.2

抽象クラスであることは、プログラムでRailwayCarを使用することを妨げるものではない。RailwayCarのオブジェクトは構築できないが、RailwayCarはインターフェースとして使用できる。サブクラスは"乗客用車両は鉄道車両である"と"貨物用車両は鉄道車両である"という2つの関係を定義しているため、PassengerCarFreightCarのオブジェクトはRailwayCarの代わりに使うことができる。これは、以下のTrainクラスで確認できる。

列車を表すクラスは、機関車と鉄道車両の配列で構成される:

class Train : RailwayVehicle {
    Locomotive locomotive;
    RailwayCar[] cars;

    // ...
}
D

重要な点を繰り返しておこう。LocomotiveRailwayCarはどちらもRailwayVehicleから継承しているが、Trainをどちらからも継承することは正しくない。継承は"である"という関係をモデル化しており、列車は機関車でも客車でもない。列車はそれらで構成されている。

すべての列車に機関車が必要であると仮定すると、Trainコンストラクタは、有効なLocomotiveオブジェクトを受け取ることを保証する必要がある。同様に、鉄道車両がオプションである場合は、メンバー関数によって追加することができる。

import std.exception;
// ...

class Train : RailwayVehicle {
    // ...

    this(Locomotive locomotive) {
        enforce(locomotive !is null,
                "Locomotive cannot be null");
        this.locomotive = locomotive;
    }

    void addCar(RailwayCar[] cars...) {
        this.cars ~= cars;
    }

    // ...
}
D

addCar()は、RailwayCarオブジェクトも検証できることに注意しよう。ここでは、その検証は無視する。

列車の出発と到着もサポートする必要があると考えられる:

class Train : RailwayVehicle {
    // ...

    void departStation(string station) {
        foreach (car; cars) {
            car.load();
        }

        writefln("Departing from %s station", station);
    }

    void arriveStation(string station) {
        writefln("Arriving at %s station", station);

        foreach (car; cars) {
            car.unload();
        }
    }
}
D

以下のmain()は、RailwayVehicle階層を利用している:

import std.stdio;

// ...

void main() {
    auto locomotive = new Locomotive;
    auto train = new Train(locomotive);

    train.addCar(new PassengerCar, new FreightCar);

    train.departStation("Ankara");
    train.advance(500);
    train.arriveStation("Haydarpaşa");
}
D
inheritance.2

Trainクラスは、2つの別々のインターフェースによって提供される関数によって使用されている。

  1. advance()関数が呼び出されると、その関数はRailwayVehicleによって宣言されているため、TrainオブジェクトがRailwayVehicleとして使用される。
  2. departStation()およびarriveStation()関数が呼び出されると、trainオブジェクトがTrainとして使用される。これは、これらの関数がTrainによって宣言されているためだ。

矢印は、load()およびunload()関数が、RailwayCarの実際の型に従って動作することを示している。

乗客が乗車している     
木箱が積み込まれている       
アンカラ駅を出発
車両は500キロメートル進んでいる
ハイダルパシャ駅に到着
乗客が降車している    
木箱が積み降ろされている     
要約
演習
  1. RailwayVehicleを変更しよう。前進した距離を報告するだけでなく、音も鳴らすようにしよう。出力を短くするために、100 キロメートルごとに音を鳴らすようにしよう。
    class RailwayVehicle {
        void advance(size_t kilometers) {
            writefln("The vehicle is advancing %s kilometers",
                     kilometers);
    
            foreach (i; 0 .. kilometers / 100) {
                writefln("  %s", makeSound());
            }
        }
    
        // ...
    }
    D

    ただし、車両によって音が異なる可能性があるため、makeSound()RailwayVehicleで定義できない:

    • Locomotiveには"チューチュー"
    • RailwayCarには"カチャカチャ"

    注釈: Train.makeSoundは次の演習で残しておいてほしい。

    オーバーライドする必要があるため、makeSound()はスーパークラスでabstractとして宣言する必要がある。

    class RailwayVehicle {
        // ...
    
        abstract string makeSound();
    }
    D

    サブクラスでmakeSound()を実装し、次のmain()でコードを試してみてみよう。

    void main() {
        auto railwayCar1 = new PassengerCar;
        railwayCar1.advance(100);
    
        auto railwayCar2 = new FreightCar;
        railwayCar2.advance(200);
    
        auto locomotive = new Locomotive;
        locomotive.advance(300);
    }
    D
    inheritance.3

    プログラムが以下の出力を生成するようにしよう。

    車両は100キロメートル進んでいる
      カチャカチャ
    車両は200キロメートル進んでいる
      カチャカチャ
      カチャカチャ
    車両は300キロメートル進んでいる
      チューチュー
      チューチュー
      チューチュー

    PassengerCarFreightCarの音が異なる必要はない。これらは、RailwayCarと同じ実装を共有することができる。

  2. makeSound()Trainで実装する方法を考えてみよう。1つのアイデアは、Train.makeSoundが、Trainのメンバーの音で構成されるstringを返すことだ。