継承を使うと、あるクラスを別のクラスの上に築けます。派生クラスは基底クラスのメンバーを得て、独自のものを加え、基底クラスが許す振る舞いを置き換えられます。virtual メソッドと組み合わせるとポリモーフィズムが得られます。基底クラスに対して書いたコードが、どの派生クラスを持っているかを知らなくても、正しい派生の振る舞いを実行します。
クラスを派生させる
基底クラスをコロンの後に書きます。派生クラスは、基底クラスが持つすべてに加えて、自分が宣言したものを持ちます。
出力:
TX-19: 20 km, 2 fares
True
派生クラスが得るものと得ないもの:
- 継承されるもの: フィールド、プロパティ、メソッド、イベント、入れ子の型。すべてが物理的にオブジェクトの中にあります。
- アクセスできるもの: 基底クラスが許すものだけで、
public、protected、internalのメンバーです。VehicleのprivateのメンバーはすべてのTaxiの中に存在しますが、Taxiのコードはその名前を使えません。だからKmはprivate setを持っています。Taxiはそれを読め、変更はDriveを通してだけ行います。 - 継承されないもの: コンストラクター。
Taxiは独自のものを宣言し、: base(plate)でVehicleのどれかに連鎖させなければなりません。両者が実行される順序はコンストラクターのページで示しています。
すべてのクラスは最終的に object から派生しています。だからすべてのオブジェクトが ToString()、Equals()、GetHashCode() を持っています。
virtualとoverride
基底クラスは、「派生クラスが独自の版を提供してよい」ことを示すためにメソッドに virtual を付けます。派生クラスは override でそれを置き換えます。オーバーライドの中では、base.Method() が基底クラスの版を呼びます。
出力:
[email] to lea@example.com: Your order 1042 has shipped today
(unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today
ループの変数の型は Notification なのに、各オブジェクトは独自の方法で出力します。これがポリモーフィズムです。n.Render(...) の呼び出しは、実行時にオブジェクトの実際の型から解決されます。また、Email の base.Render は、それ自体が仮想の Channel を使うので、基底のメソッドは generic ではなく email と表示することにも注目してください。基底クラスの中の仮想の呼び出しも、オーバーライドに届きます。
Channel が示すように、プロパティも仮想にできます。フィールドはできません。
コンパイラは両方のキーワードを求めます。virtual でないメソッドに override を書くとエラーCS0506(「cannot override inherited member ... because it is not marked virtual, abstract, or override」)になります。基底のメソッドが仮想なのに override を書かないのは警告にすぎませんが、次の節で示すように、意味がまったく変わります。
newとoverride:オーバーライドではなく隠蔽
派生クラスが、override を書かずに基底のメソッドと同じシグネチャのメソッドを宣言すると、基底のメソッドを隠します。コンパイラは警告を出し(基底のメソッドが仮想ならCS0114、そうでなければCS0108)、new キーワードを勧めます。new は警告を消しますが、隠蔽の動作はそのままです。
出力:
Sales report
Report
Draft report
b と c は同じ種類のオブジェクトなのに、異なるタイトルを表示します。new では、選ばれるメソッドは変数の型によって決まり、コンパイル時に決定されます。レポートを Report として扱うコード(リスト、メソッドの引数、フレームワークのコールバック)は、DraftReport.Title を決して見ません。それが望ましいことはほとんどありません。ポリモーフィズムには override を使います。new が存在するのは主に、自分が管理していない基底クラスに、すでに持っているメンバーと名前が衝突するメンバーが追加された場合のためです。
sealed
クラスに付けた sealed は、それからの派生を禁止します。
sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'
string はsealedで、多くのフレームワークの型も同じです。オーバーライドに付けた sealed は、そのレベルで連鎖を止めます。
class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed
継承のためにクラスを設計するには手間がかかります。何を仮想にするか、派生クラスが何に頼ってよいか、物事がどの順序で起きるかを決めなければなりません。そのように設計されていないクラスはsealedにしておくほうが安全です。sealedは後から誰も壊さずに外せますが、他の人が派生した後でsealedに戻すことはできません。sealedなクラスのメンバーの呼び出しは、ランタイムがオーバーライドが存在しないことを知っているので、わずかに速くなることもあります。
基底クラスは1つ、インターフェースはいくつでも
C#のクラスの基底クラスはちょうど1つです。class Admin : User, Employee はエラーCS1721(「cannot have multiple base classes」)です。ただし、クラスは基底クラスと並べて任意の数のインターフェースを実装できます。
class Admin : User, IAuditable, IComparable<Admin>
{
// base class first, then interfaces, in any order
}
「〜の一種であり、実装を共有する」には基底クラスを、「〜ができる」にはインターフェースを使います。基底クラスが、派生クラスに一部のメソッドを埋めさせるためだけに存在するなら、抽象クラスが適した道具です。
階層の上下へのキャスト
派生オブジェクトは、基底型が期待される場所でいつでも使えます。このアップキャストは暗黙で、失敗することはありません。逆方向のダウンキャストには明示的なキャストが必要で、オブジェクトがその型でなければ実行時に失敗します。
出力:
Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball
キャストなしで Dog d = pet; と書くとコンパイルエラー(CS0266:「An explicit conversion exists (are you missing a cast?)」)になります。コンパイラが知っているのは、pet が何らかの Animal であることだけだからです。オブジェクトが別の型かもしれないなら変数付きの is を、それ以外がバグである場合にだけ普通のキャストを使います。ダウンキャストが多いのは設計上の悪い兆候で、たいていはその振る舞いが基底クラスの仮想メソッドに属することを意味します。is の形についてはパターンマッチングのページで詳しく扱います。
継承とコレクション
ポリモーフィズムは要素ごとに働きますが、派生型のジェネリックコレクションは基底型のコレクションではありません。List<Animal> animals = new List<Dog>(); はコンパイルできません。そうするとリストが Cat を受け入れてしまうからです。読み取り専用のビューは共変なので、IEnumerable<Animal> animals = new List<Dog>(); は問題ありません。理由はジェネリックのページで説明しています。
よくある間違い
overrideを忘れる。 メソッドは警告付きでコンパイルでき、オーバーライドではなく何も言わずに隠蔽します。CS0114はエラーとして扱います。- すべてを
virtualにする。 仮想メンバーの1つ1つが、いつ呼ばれ、何を前提としてよいかについての派生クラスへの約束です。意図した拡張ポイントだけに付けます。 - 深い階層。 継承が3段や4段になると、どの版のメソッドが実行されるのかがわかりにくくなります。コンポジション、つまり別のオブジェクトを保持して呼び出すクラスのほうが、新しい段を増やすより簡単なことがよくあります。
- コンストラクターから仮想メソッドを呼ぶ。 オーバーライドは派生クラスのコンストラクターの本体より前に実行されるので、その本体が代入するフィールドはまだ既定値のままです。
- コードの再利用のためだけに継承を使う。
StackがListを継承すると、呼び出し側がスタックの途中にInsertできてしまいます。代わりにprivateなフィールドにListを保持します。
よくある質問
C#の継承はどう動きますか?
クラスはコロンの後に1つの基底クラスを指定します:class Dog : Animal。派生クラスは基底クラスのすべてのメンバー(フィールド、プロパティ、メソッド、イベント)を得て、独自のものを追加でき、基底クラスが virtual を付けたものをオーバーライドできます。コンストラクターは継承されず、private のメンバーはオブジェクトの中に存在しますが、派生クラスからはアクセスできません。
C#のvirtualとoverrideの違いは何ですか?
virtual は基底クラスのメソッドに付け、「派生クラスがこれを置き換えてよい」という意味です。override は派生クラスのメソッドに付け、置き換えを行います。両方が必要で、virtual、abstract、またはすでに override でないメソッドをオーバーライドするとエラーCS0506になります。仮想メソッドが呼ばれると、ランタイムは変数の型ではなく、オブジェクトの実際の型の版を実行します。
C#のnewとoverrideの違いは何ですか?
override は、基底クラスの変数を通してオブジェクトを持っているコードも含め、すべての呼び出し側に対して基底のメソッドを置き換えます。new は隠すだけです。オブジェクトを派生型として見るコードは新しいメソッドを呼び、基底型として見るコードは基底のメソッドを呼び続けます。そのため Base b = new Derived(); b.M(); は、override なら Derived.M を、new なら Base.M を実行します。
C#は多重継承に対応していますか?
クラスについては対応していません。クラスの基底クラスはちょうど1つで、2つ並べるとエラーCS1721になります。クラスは任意の数のインターフェースを実装でき、C#は「この型はいくつかのことができる」をこの方法で表します。C# 8以降は、インターフェースに既定のメソッドの実装を持たせることもできます。
C#のsealedとはどういう意味ですか?
sealed のクラスは基底クラスとして使えず、それから派生するとエラーCS0509になります。たとえば string はsealedです。メソッドに対する sealed override は、階層のさらに下のクラスがそれを再びオーバーライドするのを止めます(CS0239)。継承を前提に設計されていないクラスをsealedにするのは、妥当な既定の選択です。
C#で基底クラスのメソッドを呼ぶには?
派生クラスの中で、通常はオーバーライドの中で base.MethodName(...) を使います:public override string Describe() => base.Describe() + " with GPS";。コンストラクターでは、引数リストの後に : base(...) を書いて、どの基底のコンストラクターを実行するかを選びます。