継承はC#の核となる仕組みで、共通の機能を基底クラスにまとめ、派生クラスで差異だけを追加することで安全に再利用できます。
本稿ではis-aの考え方から、基底と派生の作り方、virtual
とoverride
によるポリモーフィズムまでを段階的に解説します。
C#の継承とは
is-a関係と再利用のメリット
is-a関係とは
継承は「AはBである(is-a)」が成り立つときに使います。
例えば「犬は動物である」ので、Dog
は Animal
の一種です。
このとき Dog : Animal
と宣言することで、Animal
のメンバーを Dog
が引き継げます。
再利用できるもの
継承により、基底クラスに書かれた以下の要素が派生クラスで再利用されます。
- フィールドやプロパティ(
{ get; set; }
) - メソッドの実装
- イベントや定数などのメンバー
重複コードを減らせるため、保守性が上がります。
ポリモーフィズムで動作を使い分ける
基底クラスで virtual
を付けたメソッドを、派生クラスで override
すると、同じ呼び出しでも型ごとに動作を変えられます。
これをポリモーフィズムと呼びます。
特に、List<Animal>
のように基底型で集合を扱い、各要素が自分の型に応じて振る舞う設計は、拡張に強くなります。
基底クラスの作り方
共通メンバーの切り出し方
例題の前提
ペットアプリを想定し、「名前」と「年齢」は犬でも猫でも共通です。
一方で、犬には「お気に入りのおもちゃ」、猫には「室内飼いかどうか」といった固有情報があるとします。
プロパティとメソッドの設計
共通のデータはプロパティで表し、共通の振る舞い(説明や鳴き声の枠組み)をメソッドで用意します。
後で派生クラスが上書きできるように、差し替え前提のメソッドには virtual
を付けます。
// 共通の性質を表す基底クラス
public class Animal
{
// どの動物にもある共通の情報
public string Name { get; set; } // 名前
public int Age { get; set; } // 年齢
// 後で派生クラスが具体化する前提の振る舞い
public virtual string Speak()
{
// デフォルトの鳴き声(未定義のイメージ)
return "...";
}
public virtual string Describe()
{
// 共通の説明文のひな型
return $"{Name}は{Age}歳です";
}
}
コンストラクタで初期化する理由
オブジェクトが不完全な状態で使われるのを防ぐため、生成時に必要な情報はコンストラクタで受け取り、プロパティへ代入しておくのが安全です。
これにより、生成直後から一貫して使用できる状態を保証できます。
派生クラスの作り方
継承の書き方(: 基底クラス)とサンプルコード
継承の構文
C#の継承はコロン(:
)で表します。
class 派生クラス名 : 基底クラス名
の形です。
// DogはAnimalの一種(犬は動物である)
public class Dog : Animal
{
// Dog固有の実装をここに加える
}
動くサンプル(基底と派生、ポリモーフィズムまで)
以下は、Animal
を基底に Dog
と Cat
を作り、virtual
/override
で鳴き声や説明を切り替える完全なサンプルです。
List<Animal>
に犬と猫を混在させ、同じ呼び出しで異なる振る舞いになる様子を確認します。
using System;
using System.Collections.Generic;
// 共通の性質を持つ基底クラス
public class Animal
{
// 共有される基本情報
public string Name { get; set; }
public int Age { get; set; }
// 引数なしコンストラクタ(派生から代入しやすくするため)
public Animal() { }
// 派生で上書き前提の振る舞い
public virtual string Speak()
{
return "..."; // デフォルトの鳴き声
}
public virtual string Describe()
{
return $"{Name}は{Age}歳です"; // 説明のひな型
}
}
// 犬クラス: Animalを継承
public class Dog : Animal
{
public string FavoriteToy { get; set; } // 犬固有の情報
// 生成時に必要な情報を受け取って初期化
public Dog(string name, int age, string favoriteToy)
{
// 基底クラスのプロパティへ代入(継承で再利用)
Name = name;
Age = age;
FavoriteToy = favoriteToy;
}
// 鳴き声を犬用に上書き
public override string Speak()
{
return "ワン!";
}
// 説明文を犬用に上書き
public override string Describe()
{
// base.Describe()を使わず、シンプルに書き直してもOK
return $"{Name}は{Age}歳のイヌです。お気に入りのおもちゃは{FavoriteToy}です";
}
}
// 猫クラス: Animalを継承
public class Cat : Animal
{
public bool IsIndoor { get; set; } // 室内飼いかどうか
public Cat(string name, int age, bool isIndoor)
{
Name = name;
Age = age;
IsIndoor = isIndoor;
}
public override string Speak()
{
return "ニャー";
}
public override string Describe()
{
string type = IsIndoor ? "室内飼い" : "外で遊ぶのが好き";
return $"{Name}は{Age}歳のネコです。{type}です";
}
}
public class Program
{
public static void Main()
{
// 派生クラスのインスタンスを作成(コンストラクタで初期化)
Dog dog = new Dog("ポチ", 3, "ボール");
Cat cat = new Cat("ミケ", 2, true);
// 基底型のコレクションに混在させる(ポリモーフィズムの典型例)
List<Animal> animals = new List<Animal> { dog, cat };
Console.WriteLine("=== 動作の使い分け(ポリモーフィズム) ===");
foreach (var a in animals)
{
// 同じ呼び出しでも、実体の型に応じて動作が変わる
Console.WriteLine($"{a.Describe()}。鳴き声は「{a.Speak()}」");
}
// 同じ枠(Animal)のまま、別の派生オブジェクトに差し替え可能
animals[1] = new Cat("タマ", 5, false);
Console.WriteLine("--- 差し替え後 ---");
foreach (var a in animals)
{
Console.WriteLine($"{a.Describe()}。鳴き声は「{a.Speak()}」");
}
}
}
=== 動作の使い分け(ポリモーフィズム) ===
ポチは3歳のイヌです。お気に入りのおもちゃはボールです。鳴き声は「ワン!」
ミケは2歳のネコです。室内飼いです。鳴き声は「ニャー」
--- 差し替え後 ---
ポチは3歳のイヌです。お気に入りのおもちゃはボールです。鳴き声は「ワン!」
タマは5歳のネコです。外で遊ぶのが好きです。鳴き声は「ニャー」
継承の注意点とベストプラクティス
使いどころの判断基準(チェックリスト)
判断の観点
継承が適切かどうかは、以下の観点で整理すると判断しやすくなります。
- is-aが自然に言えるか: 「XはYである」と言い換えて違和感がないか。
- 共有の大きさ: 共通プロパティやメソッドが十分に多く、重複削減の効果が高いか。
- 置換可能性: 基底型の代わりに派生型を置いても期待どおり動くか(リスコフの置換原則)。
- 変更の影響: 基底クラスの変更が全派生に波及して困らないか。破壊的変更の可能性は低いか。
- 階層の深さ: 階層が深くなりすぎていないか。一般に2〜3段以内が読みやすい。
- 可視性と拡張点: 上書きが必要なメソッドだけ
virtual
にしているか。むやみに拡張点を増やしていないか。 - ランタイムの振る舞い差: 実行時に型ごとの差し替えが本当に必要か。不要なら継承を使わない選択も検討する。
継承とコンポジションの使い分け
継承は強力ですが万能ではありません。
オブジェクトが「〜を持つ(has-a)」関係なら、別クラスをプロパティとして持つコンポジション(委譲)が適することが多いです。
使うと良い場合 | 避ける場合 |
---|---|
is-a が明確で、共通の振る舞いを派生で拡張したい | has-a 関係で、部品の差し替えや組み合わせを柔軟にしたい |
基底型のコレクションで多態的に扱いたい | 基底の変更が派生に広く影響してしまう懸念がある |
差し替え前提の操作を virtual で限定できる | 階層が深くなり複雑化しそう、責務が曖昧になりそう |
よくある落とし穴
継承で共有したくなるあまり、責務の異なるものまで1つの基底に押し込むと、後からの拡張で破綻しやすくなります。
また、virtual
を乱用すると、想定外の上書きにより不変条件が壊れることがあります。
上書きの必要がないメンバーは、通常のメソッドのままにしておくのが無難です。
小さく始めて拡張する
最初は最小限の基底クラスを用意し、共通化のメリットが見えた段階で少しずつ virtual
を増やすのが安全です。
テストを用意し、基底型のテストケースを派生型にも適用して置換可能性を定期的に確認すると品質を保てます。
まとめ
継承は、共通部分を基底クラスへ切り出し、派生クラスで違いだけを実装することで重複を避ける強力な仕組みです。
is-aの関係が自然であること、置換可能性が成り立つこと、そして上書きが必要な箇所だけを virtual
とすることが、安全で拡張しやすい設計につながります。
まずは小さな例から始め、List<Animal>
のように基底型で扱える形に整える練習を重ねることで、継承とポリモーフィズムを無理なく活用できるようになります。