C# の継承は「既存クラスの資産を引き継ぎながら機能を拡張する」仕組みです。基本的な virtual/override に加えて、override と new の決定的な違い・アップキャスト/ダウンキャスト・ポリモーフィズムの実践・継承とコンポジションの使い分けまで理解すると、設計の判断力が一段階上がります。
本記事では継承の基礎から実践的な設計原則・よくある落とし穴まで体系的に解説します。
継承の基本構文
C# では : 親クラス名 を付けてクラスを継承します。子クラスは親クラスの public・protected メンバーをそのまま使えます。
// 親クラス(基底クラス / base class)
class Vehicle
{
public string Make { get; set; } = "";
public string Model { get; set; } = "";
public int Year { get; set; }
public void StartEngine()
=> Console.WriteLine($"{Make} {Model} のエンジンをかけました");
public override string ToString()
=> $"{Year} {Make} {Model}";
}
// 子クラス(派生クラス / derived class)
class Car : Vehicle
{
public int DoorCount { get; set; } = 4;
public void OpenTrunk()
=> Console.WriteLine($"{Model} のトランクを開けました");
}
class ElectricCar : Car
{
public int BatteryCapacityKwh { get; set; }
public void Charge()
=> Console.WriteLine($"{Model} を充電中({BatteryCapacityKwh} kWh)");
}
var ev = new ElectricCar
{
Make = "Toyota", Model = "bZ4X", Year = 2024,
DoorCount = 4, BatteryCapacityKwh = 72
};
ev.StartEngine(); // Vehicle のメソッドを使える
ev.OpenTrunk(); // Car のメソッドを使える
ev.Charge(); // ElectricCar 自身のメソッド
Console.WriteLine(ev); // 2024 Toyota bZ4X
・親クラス = 基底クラス(base class)= スーパークラス
・子クラス = 派生クラス(derived class)= サブクラス
C# では1つのクラスから1つのクラスしか継承できません(単一継承)。複数の「契約」を実装したい場合はインターフェースを使います。
virtual と override によるオーバーライド
親クラスで virtual を付けたメソッドは子クラスで 上書き(オーバーライド) できます。子クラス側は override を付けて再定義します。
abstract class Shape
{
public string Color { get; set; } = "黒";
// virtual: 子クラスでオーバーライド可能
public virtual double Area() => 0;
// virtual: デフォルト実装を持つ(子クラスで上書き可能)
public virtual string Describe()
=> $"{Color}の{GetType().Name}(面積: {Area():F2})";
}
class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
// override: 親の Area() を上書き
public override double Area() => Math.PI * Radius * Radius;
}
class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double w, double h) { Width = w; Height = h; }
public override double Area() => Width * Height;
// Describe() もオーバーライドして独自の説明にする
public override string Describe()
=> $"{Color}の長方形({Width}×{Height}、面積: {Area():F2})";
}
var shapes = new Shape[]
{
new Circle(5) { Color = "赤" },
new Rectangle(4, 6) { Color = "青" },
};
foreach (var s in shapes)
Console.WriteLine(s.Describe());
// 赤のCircle(面積: 78.54)
// 青の長方形(4×6、面積: 24.00)
virtual・abstract・override の付いたメンバーだけです。これらが付いていない通常のメソッドは子クラスからオーバーライドできません(後述の new による隠蔽とは異なります)。base キーワード
base を使うと親クラスのメンバーやコンストラクタを明示的に呼び出せます。オーバーライドで「親の処理を実行してから追加処理をする」パターンで頻繁に使います。
class DataService
{
public virtual string Fetch(int id)
{
// 基本の取得処理
return $"データ#{id}";
}
}
class CachedDataService : DataService
{
private readonly Dictionary<int, string> _cache = new();
public override string Fetch(int id)
{
if (_cache.TryGetValue(id, out var cached))
{
Console.WriteLine($"[キャッシュ] id={id}");
return cached;
}
// 親クラスの処理を呼び出す
var result = base.Fetch(id);
_cache[id] = result;
Console.WriteLine($"[DB取得] id={id}");
return result;
}
}
var svc = new CachedDataService();
Console.WriteLine(svc.Fetch(1)); // [DB取得] id=1 → データ#1
Console.WriteLine(svc.Fetch(1)); // [キャッシュ] id=1 → データ#1
class Animal
{
public string Name { get; }
public string Species { get; }
public Animal(string name, string species)
{
Name = name;
Species = species;
}
}
class Dog : Animal
{
public string Breed { get; }
// : base(...) で親コンストラクタを呼び出す
public Dog(string name, string breed)
: base(name, "Canis lupus familiaris")
{
Breed = breed;
}
}
var d = new Dog("ポチ", "柴犬");
Console.WriteLine($"{d.Name} / {d.Species} / {d.Breed}");
// ポチ / Canis lupus familiaris / 柴犬
override と new の決定的な違い
「オーバーライド」と「メソッド隠蔽(new)」は見た目が似ていても動作が根本的に異なります。この違いを理解しないとポリモーフィズムが正しく機能しません。
class Base
{
public virtual void VirtualMethod()
=> Console.WriteLine("Base.VirtualMethod");
public void NormalMethod()
=> Console.WriteLine("Base.NormalMethod");
}
class Derived : Base
{
// override: 仮想メソッドを上書き(ポリモーフィズムが効く)
public override void VirtualMethod()
=> Console.WriteLine("Derived.VirtualMethod");
// new: 親のメソッドを「隠す」(ポリモーフィズムは効かない)
public new void NormalMethod()
=> Console.WriteLine("Derived.NormalMethod");
}
Derived d = new Derived();
Base b = d; // アップキャスト(同じオブジェクト)
// override の場合: 実際の型(Derived)のメソッドが呼ばれる
d.VirtualMethod(); // Derived.VirtualMethod ← Derived として呼んでも
b.VirtualMethod(); // Derived.VirtualMethod ← Base として呼んでも同じ!
// new の場合: 変数の宣言型によって呼ばれるメソッドが変わる
d.NormalMethod(); // Derived.NormalMethod ← Derived 変数からは Derived が呼ばれる
b.NormalMethod(); // Base.NormalMethod ← Base 変数からは Base が呼ばれる!
new によるメソッド隠蔽を意図せず行ってしまうと、「変数の型によって動作が変わる」という混乱が生じます。override なしで親クラスと同名のメソッドを書くとコンパイラ警告が出ます。new を明示しないと警告が出るため、意図的かどうかを常に確認しましょう。| キーワード | 対象 | 呼び出しの決定 | 動作の特徴 |
|---|---|---|---|
override |
virtual/abstract なメソッド |
実際の型(実行時型) | 常にオーバーライド版が呼ばれる |
new(隠蔽) |
どのメソッドでも可 | 変数の宣言型(コンパイル時型) | 宣言型によって呼ばれるメソッドが変わる |
sealed override でオーバーライドを止める
sealed を override に付けると、継承チェーンのそこでオーバーライドを終了できます。クラス全体に sealed を付けると継承ごと禁止です。
class Animal
{
public virtual void Speak()
=> Console.WriteLine("動物が音を出します");
}
class Dog : Animal
{
// sealed override: これ以上のオーバーライドを禁止
public sealed override void Speak()
=> Console.WriteLine("ワンワン!");
}
class Poodle : Dog
{
// コンパイルエラー: sealed なのでオーバーライドできない
// public override void Speak() { }
// 別のメソッドは追加できる
public void Groom()
=> Console.WriteLine("グルーミング中…");
}
sealed を付けたクラス自体は継承禁止になります(sealed class Poodle : Dog { })。sealed override はクラスの継承は許可しつつ特定メソッドのオーバーライドだけを禁止します。クラス全体の sealed についてはクラスとオブジェクト完全ガイドを参照してください。abstract メソッド:実装を子クラスに強制する
abstract を付けたメソッドは本体を持たず、子クラスが必ず override で実装しなければなりません。abstract メソッドを持つクラスは abstract class にする必要があります。
// abstract クラス: 直接 new できない
abstract class Report
{
// abstract: 子クラスが必ず実装する(本体なし)
public abstract string GenerateContent();
// virtual: デフォルト実装あり(任意でオーバーライド)
public virtual string GetHeader()
=> $"=== レポート ({DateTime.Now:yyyy-MM-dd}) ===";
// 通常メソッド: 共通処理(オーバーライド不可)
public void Print()
{
Console.WriteLine(GetHeader());
Console.WriteLine(GenerateContent());
}
}
class SalesReport : Report
{
private readonly decimal _total;
public SalesReport(decimal total) => _total = total;
// abstract を実装(override 必須)
public override string GenerateContent()
=> $"売上合計: {_total:C}";
}
class InventoryReport : Report
{
private readonly int _itemCount;
public InventoryReport(int count) => _itemCount = count;
public override string GenerateContent()
=> $"在庫アイテム数: {_itemCount:N0} 件";
// GetHeader() もオーバーライドして独自ヘッダーにする(任意)
public override string GetHeader()
=> "【在庫レポート】";
}
var reports = new Report[]
{
new SalesReport(1_250_000),
new InventoryReport(3842),
};
foreach (var r in reports)
r.Print();
// === レポート (2024-05-01) ===
// 売上合計: ¥1,250,000
// 【在庫レポート】
// 在庫アイテム数: 3,842 件
ポリモーフィズムとキャスト(is / as)
ポリモーフィズム(多態性)とは、「親クラス型の変数で子クラスのオブジェクトを扱い、実際の型に応じた処理を実行する」仕組みです。アップキャストとダウンキャストを理解しておく必要があります。
class Animal
{
public string Name { get; }
public Animal(string name) => Name = name;
public virtual void Speak() => Console.WriteLine($"{Name} が音を出します");
}
class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak() => Console.WriteLine($"{Name}: ワンワン!");
public void Fetch() => Console.WriteLine($"{Name} がボールを取ってきました");
}
class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void Speak() => Console.WriteLine($"{Name}: ニャー!");
}
// アップキャスト: 子 → 親(暗黙的・安全)
Animal a = new Dog("ポチ"); // Dog を Animal として扱う
a.Speak(); // Dog.Speak() が呼ばれる(ポリモーフィズム)
// a.Fetch(); // コンパイルエラー: Animal 型には Fetch がない
// ダウンキャスト: 親 → 子(明示的・失敗の可能性あり)
if (a is Dog dog)
{
dog.Fetch(); // ポチ がボールを取ってきました
}
// as: 失敗すると null を返す(例外を投げない)
var cat = a as Cat;
Console.WriteLine(cat is null ? "Cat ではない" : cat.Name); // Cat ではない
// さまざまな Shape を一括処理できる
var shapes = new List<Shape>
{
new Circle(5),
new Rectangle(4, 6),
new Circle(3),
};
// 実際の型を意識せず、共通インターフェースで処理
double totalArea = shapes.Sum(s => s.Area());
Console.WriteLine($"合計面積: {totalArea:F2}"); // 合計面積: 144.27
// is パターンマッチングで型ごとに処理
foreach (var s in shapes)
{
if (s is Circle c)
Console.WriteLine($"円: 半径={c.Radius}");
else if (s is Rectangle r)
Console.WriteLine($"長方形: {r.Width}×{r.Height}");
}
| 方向 | 構文例 | 変換 | 失敗時 | 特徴 |
|---|---|---|---|---|
| アップキャスト | Animal a = new Dog(); |
自動(暗黙的) | 常に安全 | 親クラスのメンバーのみアクセス可 |
| ダウンキャスト | (Dog)a |
明示的 | 型が合わなければ InvalidCastException | 対象型のメンバーにアクセス可 |
is パターン |
if (a is Dog d) |
型チェック+キャスト | 失敗は null(例外なし) | 最も安全・推奨 |
as |
Dog d = a as Dog; |
明示的 | 失敗は null(例外なし) | 後で null チェックが必要 |
継承 vs コンポジション:どちらを選ぶか
継承は強力ですが「is-a 関係」が成り立つときにのみ使うべきです。単に機能を使いまわしたいだけならコンポジション(has-a 関係)の方が保守性が高くなります。
// ── 継承(is-a)が適切な例 ──
// Dog は Animal の一種 → 継承が自然
class Animal2 { public virtual void Breathe() => Console.WriteLine("呼吸"); }
class Dog2 : Animal2 { public void Bark() => Console.WriteLine("ワン"); }
// ── コンポジション(has-a)が適切な例 ──
// Logger は Car の一種ではない → 継承は不自然
// BAD: 継承で Logger 機能を追加(Logger は Car ではない)
// class LoggableCar : Car { ... }
// GOOD: コンポジションで Logger を持つ
class Logger
{
public void Log(string msg) => Console.WriteLine($"[LOG] {msg}");
}
class Car2
{
private readonly Logger _logger = new();
public string Model { get; }
public Car2(string model) => Model = model;
public void Drive()
{
_logger.Log($"{Model} が走り出しました");
}
}
var car = new Car2("Prius");
car.Drive(); // [LOG] Prius が走り出しました
| 選択 | 使うべき状況① | 使うべき状況② | 使うべき状況③ |
|---|---|---|---|
| 継承(is-a) | 「Dog は Animal の一種」が成り立つ | コードの多態性が必要 | 抽象メソッドで実装を強制したい |
| コンポジション(has-a) | 「Car は Logger を持つ」関係 | テスト容易性(モック差し替え)が必要 | 複数の機能を組み合わせたい |
Liskov 置換原則(LSP):継承の正しい使い方
Liskov 置換原則とは「子クラスは親クラスとして使えなければならない」という原則です。子クラスが親クラスの「約束事(事前条件・事後条件)」を破ると LSP 違反になり、バグや設計の破綻が起きます。
// ── LSP 違反の例 ──
class Rectangle2
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
class Square : Rectangle2
{
// 正方形は幅と高さを連動させる
public override int Width
{
get => base.Width;
set { base.Width = value; base.Height = value; } // Height も変える
}
public override int Height
{
get => base.Height;
set { base.Width = value; base.Height = value; }
}
}
// 親クラス(Rectangle2)として使うと壊れる
Rectangle2 r = new Square();
r.Width = 5;
r.Height = 3;
Console.WriteLine(r.Area); // 9 ← 期待値は 15!(Height の set が Width も変えてしまう)
// ── 対処: 共通の抽象クラスやインターフェースで統一 ──
interface IShape2 { int Area { get; } }
class Rectangle3 : IShape2 { public int Width, Height; public int Area => Width * Height; }
class Square2 : IShape2 { public int Side; public int Area => Side * Side; }
throw new NotImplementedException() で親クラスのメソッドを無効化するのも典型的な LSP 違反です。よくある落とし穴と注意点
override と new を混同して意図しない動作になる
親クラスに同名のメソッドが存在するのに override を付け忘れると、コンパイラは new による隠蔽として扱います。警告が出るのでそのまま無視せず、override か new のどちらが意図に合っているかを明示してください。親クラス変数から呼んだ際に「なぜ親の実装が動くのか」でバグに気づくパターンがよくあります。
ダウンキャストで InvalidCastException が発生する
(Dog)animal のような直接キャストは、実際の型が Dog でない場合に InvalidCastException を投げます。is パターンマッチング(if (animal is Dog d))か as + null チェックを使うと安全です。特に外部から受け取ったオブジェクトをキャストする場面では必ず型確認を行いましょう。
コンストラクタ内で virtual メソッドを呼ぶ
親クラスのコンストラクタ内で virtual メソッドを呼び出すと、子クラスの override 版が実行されます。しかし子クラスのフィールドはまだ初期化されていないため、予期しない動作(null 参照・デフォルト値)が起きます。コンストラクタ内では仮想メソッドの呼び出しを避け、ファクトリメソッドや別の初期化メソッドに切り出しましょう。
深い継承チェーンで基底クラスの変更が波及する
継承を3〜4階層以上積み重ねると、基底クラスの修正が全派生クラスに影響します。「どのクラスが何をオーバーライドしているか」の把握が困難になり、バグの温床になります。継承チェーンが深くなってきたらコンポジションへのリファクタリングを検討してください。
よくある質問
override を使います。new による隠蔽は「変数の宣言型によって動作が変わる」という混乱を生み、ポリモーフィズムが効かなくなります。new が有効な場面は「意図的に親クラスとは独立した全く別のメソッドを提供し、ポリモーフィズムを使わないことが確定している」場合のみです。is パターンマッチング(if (obj is Dog d) { ... })が最も推奨されます。型チェックとキャストが1行で済み、型が合わなければブロックに入らないため安全です。as は「null を返すことを使いたい」場面、例えば switch 外でキャストを試みて後で null チェックする場合に使います。直接キャスト (Dog)obj は型が確実な場合のみ使い、不確かな場合は避けてください。string クラスが典型的な sealed クラスです。まとめ
| キーワード / 概念 | 説明 | ポイント |
|---|---|---|
継承(: 親クラス) |
親の public/protected メンバーを引き継ぐ | is-a 関係が成り立つときに使う |
virtual |
子クラスでオーバーライド可能にする | デフォルト実装を持つ |
override |
親の virtual/abstract メソッドを上書き | ポリモーフィズムが効く(実行時型で決定) |
new(隠蔽) |
親と同名のメソッドを宣言型で隠す | ポリモーフィズムは効かない(宣言型で決定) |
base |
親クラスのメソッド/コンストラクタを呼ぶ | オーバーライドで親処理を残しつつ拡張するときに使う |
abstract |
実装を子クラスに強制(本体なし) | abstract クラスに含める必要がある |
sealed override |
継承チェーンのその時点でオーバーライドを禁止 | クラス sealed とは別(継承は可能) |
| ポリモーフィズム | 親型変数で子型オブジェクトを扱い実際の型の処理を実行 | override + アップキャストで実現 |
| is / as | 安全なダウンキャスト | is パターンマッチングが最も推奨 |
| コンポジション | 継承ではなくフィールドとして持つ | has-a 関係・テスト容易性・柔軟性 |
アクセス修飾子(public/private/protected)の詳細はカプセル化とアクセス修飾子を、インターフェースと抽象クラスの選択はインターフェースと抽象クラスの違いを参照してください。