閉じる

【C#】継承とは?メリット・注意点・実装パターンを具体例で解説

C#の継承は、オブジェクト指向らしいコードを書くうえで欠かせない仕組みです。

ただし、なんとなく使ってしまうと可読性や保守性を大きく下げてしまいます。

本記事では、C#の継承の基本からメリット・注意点、実装パターンまでを具体的なコード例とともに丁寧に解説します。

【C#】継承とは?基本概要

継承とは何か

継承とは、あるクラス(親クラス、基底クラス)の機能を、別のクラス(子クラス、派生クラス)が引き継いで使えるようにする仕組みのことです。

親クラスに共通処理をまとめ、子クラスでは差分だけを書くことで、重複コードを減らしつつ拡張しやすい設計にできます。

C#では、以下のように:を使って継承を表現します。

C#
// 親クラス(基底クラス)
public class Animal
{
    public string Name { get; set; }

    // 仮想メソッド: 子クラスで上書き可能
    public virtual void Speak()
    {
        Console.WriteLine("なにかの動物が鳴いています...");
    }
}

// 子クラス(派生クラス)
public class Dog : Animal
{
    public string Breed { get; set; }

    // オーバーライド: 親クラスの振る舞いを上書き
    public override void Speak()
    {
        Console.WriteLine("ワン! ワン!");
    }
}

public class Program
{
    public static void Main()
    {
        // DogはAnimalを継承しているので、Nameプロパティをそのまま使える
        Dog dog = new Dog
        {
            Name = "ポチ",
            Breed = "柴犬"
        };

        Console.WriteLine($"名前: {dog.Name}");
        Console.WriteLine($"犬種: {dog.Breed}");
        dog.Speak(); // 上書きされたメソッドが呼ばれる
    }
}
実行結果
名前: ポチ
犬種: 柴犬
ワン! ワン!

「DogはAnimalである」(is-a関係)が成り立つときに継承を使うのが基本方針です。

継承のメリット

コードの再利用性向上

継承を用いると、共通のプロパティやメソッドを親クラスに集約できます。

これにより、修正が必要になったときに1か所を変更するだけで済むため、保守性が高まります。

C#
public class BaseCharacter
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Mp { get; set; }

    public virtual void Attack()
    {
        Console.WriteLine($"{Name} は攻撃した!");
    }

    public void Defend()
    {
        Console.WriteLine($"{Name} は防御した!");
    }
}

public class Warrior : BaseCharacter
{
    public void PowerSlash()
    {
        Console.WriteLine($"{Name} はパワースラッシュを放った!");
    }
}

public class Mage : BaseCharacter
{
    public void FireBall()
    {
        Console.WriteLine($"{Name} はファイアボールを唱えた!");
    }
}

public class Program
{
    public static void Main()
    {
        Warrior w = new Warrior { Name = "アルト", Hp = 100, Mp = 20 };
        Mage m = new Mage { Name = "リナ", Hp = 70, Mp = 80 };

        w.Attack();
        w.PowerSlash();
        m.Attack();
        m.FireBall();
    }
}
実行結果
アルト は攻撃した!
アルト はパワースラッシュを放った!
リナ は攻撃した!
リナ はファイアボールを唱えた!

AttackやDefendの処理を、各クラスに重複して書く必要がない点がメリットです。

共通インターフェースによる扱いやすさ

親クラス型で子クラスのインスタンスを扱えるため、コレクション管理が容易になります。

C#
public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("ニャー");
    }
}

public class Bird : Animal
{
    public override void Speak()
    {
        Console.WriteLine("チュンチュン");
    }
}

public class Program
{
    public static void Main()
    {
        List<Animal> animals = new List<Animal>
        {
            new Dog { Name = "ポチ" },
            new Cat { Name = "タマ" },
            new Bird { Name = "ピヨ" }
        };

        foreach (Animal animal in animals)
        {
            Console.Write($"{animal.Name}: ");
            animal.Speak(); // 実際の型に応じて動作が変わる(ポリモーフィズム)
        }
    }
}
実行結果
ポチ: ワン! ワン!
タマ: ニャー
ピヨ: チュンチュン

親クラス型のリスト1つで、異なる種類のオブジェクトを共通インターフェースとして扱える点は、設計上の大きな利点です。

継承の注意点・落とし穴

C#のクラス継承は単一継承のみ

C#では、クラスは1つのクラスしか継承できません

多重継承ができない理由は、複数の親クラスに同名メンバがある場合に、どれを使うか曖昧になってしまうためです。

ただし、C#ではインターフェースを複数実装することで、柔軟な設計が可能です。

これについては後半で触れます。

「なんでも継承」は危険

継承を多用しすぎると、 ・クラス階層が深くなり、どこで何が定義されているか分かりにくくなる ・親クラスの仕様変更が子クラス全体に波及してしまう といった問題が発生します。

「AはBである(is-a)と言えるか?」を常に意識して、安易な継承は避けることが重要です。

継承より合成を優先する原則

「継承より合成を優先せよ」という有名な設計原則があります。

継承の代わりに、「部品として他クラスを持つ(has-a関係)」設計を選ぶことで、依存関係を限定しやすくなります。

実装の基本パターン

virtual / override による基本的なオーバーライド

親クラス側でvirtualを付けると、子クラス側でoverrideによる上書きが可能になります。

C#
public class Animal
{
    public string Name { get; set; }

    public virtual void Speak()
    {
        Console.WriteLine("Animal が鳴きます。");
    }
}

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog が吠えます。ワン!");
    }
}

ポリモーフィズム(多態性)を実現するには、virtual/overrideを正しく使うことが不可欠です。

base で親クラスの実装を呼ぶ

子クラスで振る舞いを追加しつつ、元の処理も生かしたい場合はbaseキーワードを使います。

C#
public class LoggingDog : Dog
{
    public override void Speak()
    {
        // 追加処理: ログ出力
        Console.WriteLine("ログ: Speak() が呼ばれました。");

        // 親クラス(Dog)の実装を呼ぶ
        base.Speak();
    }
}

public class Program
{
    public static void Main()
    {
        LoggingDog dog = new LoggingDog { Name = "ジョン" };
        dog.Speak();
    }
}
実行結果
ログ: Speak() が呼ばれました。
Dog が吠えます。ワン!

「完全に挙動を変える」のか「元の挙動に追加する」のかで、base呼び出しの有無を検討します。

sealed でこれ以上の継承を禁止する

sealed修飾子を使うと、それ以上派生クラスを作れなくなります。

想定外の継承を禁止し、クラスの振る舞いを固定したいときに有効です。

C#
public sealed class ConfigManager
{
    public void Load()
    {
        Console.WriteLine("設定を読み込みました。");
    }
}

// 以下はコンパイルエラーになる:
// public class CustomConfigManager : ConfigManager { }

オーバーライドされたメソッドをこれ以上オーバーライドさせたくない場合は、メソッドにsealed overrideを付けます。

C#
public class Base
{
    public virtual void DoWork()
    {
        Console.WriteLine("Base の処理");
    }
}

public class Middle : Base
{
    public sealed override void DoWork()
    {
        Console.WriteLine("Middle の処理(これ以上変更させない)");
    }
}

public class Leaf : Middle
{
    // 以下はコンパイルエラー:
    // public override void DoWork() { ... }
}

クラス継承とインターフェース実装の使い分け

インターフェースで「できること」を表す

インターフェースは「このメソッドを必ず持ちます」という約束だけを定義する型です。

C#
public interface IShape
{
    double GetArea();
    void Draw();
}

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

    public double GetArea() => Math.PI * Radius * Radius;

    public void Draw()
    {
        Console.WriteLine($"半径 {Radius} の円を描画します。");
    }
}

public class Rectangle : IShape
{
    public double Width  { get; set; }
    public double Height { get; set; }

    public double GetArea() => Width * Height;

    public void Draw()
    {
        Console.WriteLine($"幅 {Width}, 高さ {Height} の四角形を描画します。");
    }
}
実行結果
(実行例は呼び出し側によって異なりますが、共通して IShape 型として扱える)

インターフェースは実装を持たないため、継承とは異なり「コードを再利用する」目的ではなく「共通の契約を表す」ために使います。

使い分けの目安

継承とインターフェースの違いを、簡単な表にまとめます。

観点クラス継承インターフェース
関係性is-a(〜は〜である)can-do(〜できる)
目的実装の再利用、ポリモーフィズム共通の契約(メソッド群)を表現
1つのクラスのみ継承可能複数実装可能
実装の有無実装を持つ原則メンバ定義のみ(※既定実装など例外あり)

・共通の基本機能をまとめたい → 継承 ・共通インターフェースでまとめて扱いたい → インターフェース と考えると整理しやすくなります。

よくある継承パターンとアンチパターン

テンプレートメソッド風の継承パターン

処理の流れは共通だが、途中の詳細だけが違う場合に使えるのが、テンプレートメソッドパターン風の構成です。

C#
public abstract class ReportBase
{
    // テンプレートメソッド: 全体の流れを定義
    public void Generate()
    {
        LoadData();
        Format();
        Output();
    }

    protected virtual void LoadData()
    {
        Console.WriteLine("共通データを読み込みます。");
    }

    protected abstract void Format();  // 各レポートで実装
    protected abstract void Output();  // 各レポートで実装
}

public class PdfReport : ReportBase
{
    protected override void Format()
    {
        Console.WriteLine("PDF形式にフォーマットします。");
    }

    protected override void Output()
    {
        Console.WriteLine("PDFファイルとして出力します。");
    }
}

public class ExcelReport : ReportBase
{
    protected override void Format()
    {
        Console.WriteLine("Excel形式にフォーマットします。");
    }

    protected override void Output()
    {
        Console.WriteLine("Excelファイルとして出力します。");
    }
}

public class Program
{
    public static void Main()
    {
        ReportBase pdf   = new PdfReport();
        ReportBase excel = new ExcelReport();

        Console.WriteLine("=== PDFレポート ===");
        pdf.Generate();

        Console.WriteLine("=== Excelレポート ===");
        excel.Generate();
    }
}
実行結果
=== PDFレポート ===
共通データを読み込みます。
PDF形式にフォーマットします。
PDFファイルとして出力します。
=== Excelレポート ===
共通データを読み込みます。
Excel形式にフォーマットします。
Excelファイルとして出力します。

処理フローは変えずに、一部の振る舞いのみ差し替えたいときに有効です。

アンチパターン: 「なんでもかんでもBase」

つい次のようなクラスを作りがちです。

C#
public class UserBase
{
    public int Id { get; set; }
    public string Name { get; set; }

    // すべてのユーザーで必要とは限らないメソッドが混ざり始める
    public virtual void SendMail() { /* ... */ }
    public virtual void CalculateSalary() { /* ... */ }
    public virtual void HasAdminPermission() { /* ... */ }
}

こうした「なんでも入りのBaseクラス」は、 ・不要なメソッドを子クラスが引き継いでしまう ・責務が増え続け、クラスが肥大化する という問題を招きます。

この場合、 ・権限まわりは別クラス(またはインターフェース)に切り出す ・給与計算ロジックも別クラスとして合成する など、責務ごとに分割する方が健全です。

まとめ

継承は、C#でオブジェクト指向らしい設計を行うための重要な仕組みです。

親クラスに共通処理を集約し、子クラスでは差分だけを実装することで、コードの再利用性と保守性を高められます。

一方で、なんでも継承に頼ると、クラス階層が肥大化し、変更に弱い設計になってしまいます。

「本当にis-a関係か」「インターフェースや合成で代用できないか」を常に確認しながら、virtual/override、base、sealedなどを適切に組み合わせていくことが、継承を上手に使いこなすコツです。

継承・ポリモーフィズム・インターフェース

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!