C#は、プログラムの再利用性や拡張性を高めるために設計されたオブジェクト指向プログラミング言語です。

その核心となる機能の一つが継承(Inheritance)です。

継承を正しく理解し、使いこなすことは、保守性の高い高品質なコードを記述するために欠かせません。

継承とは、あるクラス(親クラス)の性質を別のクラス(子クラス)が引き継ぐ仕組みのことです。

これにより、共通の機能を何度も実装する手間が省け、コードの重複を劇的に減らすことができます。

しかし、安易な継承はクラス間の結合度を強め、逆にシステムの柔軟性を損なう「継承の罠」を招くこともあります。

本記事では、C#における継承の基本概念から、具体的な構文、オーバーライドによる振る舞いの変更、そして現場で重要視されるインターフェースとの使い分けまでを徹底的に解説します。

これからC#をマスターしようとしているエンジニアの方はもちろん、改めて設計指針を見直したい中級者の方にとっても、実務で役立つ知識を網羅しています。

C#における継承の基本概念

継承を理解する上で最も重要なキーワードは、「Is-A関係」です。

「Car(車) Is A Vehicle(乗り物)」や「Dog(犬) Is An Animal(動物)」といった、特殊と一般の関係性をコードで表現するのが継承の本来の役割です。

基本クラスと派生クラスの定義

継承元となるクラスを基本クラス(Base Class)または親クラスと呼び、継承する側のクラスを派生クラス(Derived Class)または子クラスと呼びます。

C#で継承を実装するには、派生クラス名の後ろにコロン : を記述し、その後に基本クラス名を指定します。

C#
using System;

// 基本クラス(親クラス)
public class Vehicle
{
    public string Brand { get; set; } = "Unknown";

    public void Move()
    {
        Console.WriteLine($"{Brand}が移動しています。");
    }
}

// 派生クラス(子クラス)
public class Car : Vehicle
{
    public int Speed { get; set; }

    public void ShowSpeed()
    {
        Console.WriteLine($"{Brand}の速度は時速 {Speed} kmです。");
    }
}

class Program
{
    static void Main()
    {
        // 派生クラスのインスタンス化
        Car myCar = new Car();
        
        // 基本クラスから引き継いだプロパティとメソッドを使用
        myCar.Brand = "Toyota";
        myCar.Move();

        // 派生クラス独自のプロパティとメソッドを使用
        myCar.Speed = 60;
        myCar.ShowSpeed();
    }
}
実行結果
Toyotaが移動しています。
Toyotaの速度は時速 60 kmです。

この例では、Car クラスは Vehicle クラスを継承しているため、Vehicle で定義された BrandMove() をそのまま利用できています。

C#の継承における重要な制約

C#の継承には、設計ミスを防ぐための重要なルールがいくつか存在します。

単一継承の原則

C#のクラスは一度に一つのクラスしか継承できません。

複数の親を持つ多重継承は禁止されています。

これは、複数の親クラスで同名のメソッドが存在した場合にどちらを優先するかが不明になる菱形継承問題(ダイアモンド問題)を避けるためです。

なお、クラスは複数のインターフェイスを実装できるため、インターフェイスによる機能の組み合わせは可能です。

すべてのクラスはObjectを継承している

明示的に継承を指定しなくても、すべてのクラスは暗黙的に System.Object を継承します。

したがって、すべてのオブジェクトで ToString()Equals()GetHashCode() などのメソッドが使用可能です。

アクセス修飾子と継承の関係

継承において、親クラスのメンバー(フィールドやメソッド)が子クラスからどこまで見えるかを制御するのがアクセス修飾子です。

protected 修飾子の役割

継承において最も多用されるのが protected です。

修飾子説明
publicどこからでもアクセス可能。
private同一クラス内からのみアクセス可能。子クラスからはアクセス不可。
protected同一クラス内および派生クラス内からのみアクセス可能。
internal同一アセンブリ内からのみアクセス可能。

派生クラスに対してのみ公開し、外部(インスタンスを利用する側)からは隠蔽したいロジックがある場合に protected を使用します。

カプセル化と継承の両立

基本クラスのフィールドを private に設定し、子クラスからアクセスさせたい場合は、protected なプロパティを経由させるのが安全な設計です。

これにより、基本クラス側でデータの妥当性チェックを行うことが可能になります。

コンストラクタの動作と base キーワード

継承関係にあるクラスをインスタンス化する際、コンストラクタの実行順序には決まりがあります。

必ず「基本クラスのコンストラクタ」から先に実行」されます。

base キーワードによる親コンストラクタの呼び出し

基本クラスに引数付きのコンストラクタしかない場合、派生クラスのコンストラクタで明示的に親のコンストラクタを呼び出す必要があります。

これには base キーワードを使用します。

C#
public class Person
{
    public string Name { get; }

    // 基本クラスのコンストラクタ
    public Person(string name)
    {
        this.Name = name;
        Console.WriteLine("Personのコンストラクタが呼ばれました。");
    }
}

public class Employee : Person
{
    public int EmployeeId { get; }

    // baseキーワードで親のコンストラクタに引数を渡す
    public Employee(string name, int id) : base(name)
    {
        this.EmployeeId = id;
        Console.WriteLine("Employeeのコンストラクタが呼ばれました。");
    }

    public void Introduce()
    {
        Console.WriteLine($"名前: {Name}, 社員番号: {EmployeeId}");
    }
}

class Program
{
    static void Main()
    {
        Employee emp = new Employee("山田太郎", 1001);
        emp.Introduce();
    }
}
実行結果
Personのコンストラクタが呼ばれました。
Employeeのコンストラクタが呼ばれました。
名前: 山田太郎, 社員番号: 1001

このように、base(name) を通じて親クラスに必要な初期値を渡すことで、オブジェクト全体の整合性を保ちます。

メソッドのオーバーライドと多態性(ポリモーフィズム)

継承の最大のメリットは、親クラスで定義したメソッドの振る舞いを子クラスで書き換えられるオーバーライド(Override)にあります。

これにより、異なる型を同じ親の型として扱いながら、それぞれ固有の動きをさせることが可能になります。

virtual と override の使い方

C#では、上書きを許可するメソッドには virtual 修飾子を付け、上書きする側には override 修飾子を付けます。

C#
public class Animal
{
    // virtualキーワードで上書きを許可
    public virtual void MakeSound()
    {
        Console.WriteLine("動物が鳴いています。");
    }
}

public class Dog : Animal
{
    // overrideキーワードで具体化
    public override void MakeSound()
    {
        Console.WriteLine("ワンワン!");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("ニャー!");
    }
}

多態性の活用

オーバーライドを定義すると、派生クラスのインスタンスを基本クラスの型として変数に格納し、共通のインターフェースで操作できます。

C#
class Program
{
    static void Main()
    {
        // 異なる子クラスを共通の親クラスのリストとして扱う
        Animal[] animals = new Animal[]
        {
            new Dog(),
            new Cat(),
            new Animal()
        };

        foreach (var animal in animals)
        {
            // インスタンスの実際の型に応じたメソッドが実行される
            animal.MakeSound();
        }
    }
}
実行結果
ワンワン!
ニャー!
動物が鳴いています。

これを多態性(ポリモーフィズム)と呼びます。

呼び出し側は相手が「犬」か「猫」かを意識せず、単に「動物」として MakeSound() を呼ぶだけで済むため、新しい動物の種類(クラス)が増えても呼び出し側のコードを修正する必要がありません。

sealed による継承の禁止

逆に、これ以上継承されたくない、あるいはオーバーライドを禁止したい場合には sealed 修飾子を使用します。

  • クラスに付与:クラスそのものを継承不可能にします。
  • メソッドに付与:オーバーライドしたメソッドを、さらにその先の子クラスで上書きできないようにします。

特にパフォーマンス向上や、セキュリティ上の理由でクラスの設計を固定したい場合に有効です。

抽象クラス(abstract class)の活用

「動物」という抽象的な概念そのものをインスタンス化(new Animal())するのは、設計上不自然な場合があります。

このような「共通の枠組み」だけを提供し、具体的な中身は子クラスに強制させる仕組みが抽象クラス(Abstract Class)です。

抽象クラスと抽象メソッドの定義

抽象クラスには abstract 修飾子を付けます。

また、実装を持たずシグネチャのみを定義する「抽象メソッド」を持つことができます。

C#
public abstract class Shape
{
    // 抽象メソッド(実装を持たず、子クラスに実装を強制する)
    public abstract double CalculateArea();

    public void Display()
    {
        Console.WriteLine($"面積は {CalculateArea()} です。");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius) => Radius = radius;

    // 抽象メソッドの実装
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

抽象クラスを継承した派生クラスは、すべての抽象メソッドを実装しない限り、コンパイルエラーとなります。

これにより、「特定のメソッドを必ず持っていること」を保証できます。

継承 vs インターフェース

C#において継承と並んで重要なのがインターフェース(Interface)です。

どちらも「多態性を実現する」という点では似ていますが、その使い分けが設計の成否を分けます。

主な違いの比較表

特徴継承(クラス継承)インターフェース
関係性Is-A(~は~である)Can-Do(~ができる)
複数適用不可(単一継承のみ)可能(多重実装が可能)
実装の有無基本クラスに処理を書ける原則として定義のみ(C# 8.0以降はデフォルト実装可)
状態の保持フィールド(変数)を持てるフィールドを持てない
目的コードの再利用と分類振る舞いの契約と疎結合

どちらを使うべきか?

継承を使用すべきケースは、「密接な共通性」があるときです。

例えば「すべての社員は氏名と給与計算ロジックを持つ」という場合はクラス継承が適しています。

一方、インターフェースを使用すべきケースは、「異なる種類のオブジェクトに共通の能力を与える」ときです。

例えば「ファイル」「データベース」「ネットワーク」という全く異なる対象に対して「保存ができる(ISaveable)」という能力を共通化したい場合はインターフェースが最適です。

現代のソフトウェア開発では、継承による硬直化を避けるため、「継承より委譲(Composition over Inheritance)」という原則が推奨されることが多いです。

継承は非常に強力ですが、まずはインターフェースで解決できないかを検討するのが良い習慣です。

継承における注意点とベストプラクティス

継承を使いこなすためには、以下の注意点を意識する必要があります。

1. リスコフの置換原則(LSP)の遵守

「親クラスのインスタンスを子クラスのインスタンスに置き換えても、プログラムが正しく動作しなければならない」という原則です。

例えば、Bird クラスに Fly() メソッドがある場合、それを継承した Penguin クラスで Fly() を呼び出して例外を投げるような設計は、LSP違反です。

この場合、FlyableBirdNonFlyableBird に分けるか、インターフェースを利用すべきです。

2. 深すぎる継承ツリーを避ける

継承が3階層、4階層と深くなると、あるメソッドがどこで定義されているのかを追うのが困難になります。

また、親クラスの些細な変更が予想外の広範囲に影響(波及効果)を及ぼすようになります。

原則として、継承の深さは2〜3段階にとどめるのが理想的です。

3. new キーワードによる隠蔽(シャドウイング)に注意

子クラスで override を使わず、親と同じ名前のメソッドを定義すると、親のメソッドが「隠蔽」されます。

これはバグの原因になりやすいため、意図的な場合を除いて避けるべきです。

意図的な場合は new 修飾子を付与しますが、基本的には override が可能な設計を心がけましょう。

まとめ

C#の継承は、重複を排除し、多態性を利用した柔軟なプログラムを構築するための非常に強力な道具です。

  • 継承(Is-A関係)を用いて、基本クラスの資産を派生クラスで活用する。
  • protected で適切なカプセル化を行い、base で親の初期化を確実に行う。
  • virtual / override を駆使して、実行時に動的な振る舞いを実現する。
  • abstract で設計の枠組みを強制し、interface で柔軟な契約を定義する。

これらの仕組みを正しく組み合わせることで、変更に強く、読みやすいコードを書くことができます。

しかし、継承は「強力すぎるがゆえの脆さ」も併せ持っています。

密結合を避けるためにインターフェースやコンポジション(委譲)も視野に入れつつ、適材適所で継承を活用していきましょう。

この記事が、あなたのC#プログラミングにおける設計スキルの向上に役立てば幸いです。