ポリモーフィズムとは?

同じ「呼び出し⽅」で、オブジェクトごとにふるまいを切り替えられるしくみ

C# でいうポリモーフィズム(多態性)は、共通の型(基底クラスやインターフェース)を通じて、実際には派⽣ごとに異なるメソッドが動くことを指します。

ひとつのリモコンでテレビもエアコンも操作できる」——これがポリモーフィズムのイメージです。


ざっくり 3 ステップで理解する

ステップゴールキーワード
1基底クラスを作り、共通 API を決めるvirtual
2派生クラスでふるまいを変えるoverride
3共通のリストに混ぜて呼び出す実体は何でも OK

1. 基底クラスを作る ― “リモコン” を設計

「リモコンで Speak() を押すと犬は『ワン!』猫は『ニャー!』と鳴く」ポリモーフィズムのイメージです。リモコンひとつで動的に振る舞いが切り替わるイメージが伝わるよう、女の子が Speak() ボタンを押すと柴犬と三毛猫がそれぞれの鳴き声を出すイラストになります。

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("???");
    }
}
  • 共通 API:Speak()
  • virtual が「上書きできますよ」のサイン

2. 派生クラスを増やす ― “家電ラインナップ” を揃える

class Dog : Animal
{
    public override void Speak() => Console.WriteLine("ワン!");
}

class Cat : Animal
{
    public override void Speak() => Console.WriteLine("ニャー!");
}
  • override で基底実装を差し替え
  • is-a 関係
    • Dog is-a Animal
    • Cat is-a Animal

3. 共通のコレクションに混ぜる ― “同じボタンで動く!”

// 明示的に型を書いた版
List<Animal> zoo = new List<Animal>
{
    new Dog(),
    new Cat()
};

foreach (Animal a in zoo)
{
    a.Speak();   // ← 静的型は Animal、動的型は Dog / Cat
}
/* 実行結果
ワン!
ニャー!
*/

★ ポリモーフィズムの核心

  • 静的(コンパイル時)型 = Animal
    • 変数 a が「Animal 型ですよ」とコンパイラに宣言されている
    • つまり “どんなメソッドが呼べるか” は Animal クラスを基準に決まる
  • 動的(実行時)型 = Dog または Cat
    • ループ中に格納されている “中身” は Dog だったり Cat だったりする
    • virtual / override の仕組みで 実際のオブジェクトの型 を見てメソッドを選択
    • 結果として 同じ a.Speak() 呼び出し が 違うふるまい に切り替わる

ラベルと荷物のたとえ

  • 変数は「Animal と書かれた箱」(静的型)
  • 中に入っている荷物は「Dog の人形」や「Cat の人形」(動的型)
  • 箱には “鳴かせるボタン” が 1 つだけ。でも押すと中身によって声が変わる——これがポリモーフィズム!

なぜ便利? ― 3 つの実益

  1. 拡張性が高い
    • 新しい動物クラスを追加しても既存コードは変更不要
  2. 保守性が上がる
    • 共通 API に依存し、差分は派生クラス内に閉じ込められる
  3. テストが楽
    • ダミー(モック)を派生クラスとして差し込める

インターフェース版:ふるまいだけを約束する

interface IDamageable
{
    void TakeDamage(int amount);
}

class Enemy : IDamageable
{
    public void TakeDamage(int amount) =>
        Console.WriteLine($"敵に {amount} ダメージ!");
}

class Barrel : IDamageable
{
    public void TakeDamage(int amount) =>
        Console.WriteLine($"タルが爆発した!({amount})");
}
  • 重い継承ツリー が不要ならインターフェースを検討
  • 複数実装可 → class BossEnemy : Enemy, IDamageable, IDisposable { … }

よくある疑問 Q&A

QA
基底クラスに実装を書かないとダメ?abstract メソッドにすれば「必ず上書き」を強制できる
new と override の違いは?new は隠蔽。基底型で呼ぶと旧メソッドが動き、ポリモーフィズムは起きない
パフォーマンスは?仮想メソッド呼び出しは間接参照 1 回ぶん遅いが、通常は気にしなくてよい

練習課題 ✍️

  1. 【問題 A】
    • Animal を継承した Sheep クラスを追加し、Speak() で「メェー!」と鳴かせよ。
  2. 【問題 B】
    • IDamageable に Heal(int amount) を追加し、Enemy と Barrel で実装せよ。
  3. 【問題 C】
    • List<IDamageable> を作り、TakeDamage と Heal を交互に呼び出すテストコードを書け。

練習問題 【サンプル解答】

以下は 問題 A〜C を 1 つのプログラムにまとめた例です。

※見やすいように 変更点にはコメント を付けています。

using System;
using System.Collections.Generic;

#region 既存コード --------------------------------------------------
abstract class Animal                 // ← Speak を必ず実装させたいので abstract
{
    public abstract void Speak();
}

class Dog : Animal
{
    public override void Speak() => Console.WriteLine("ワン!");
}

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

#region 【問題 A】Sheep クラスを追加 -------------------------------
class Sheep : Animal
{
    public override void Speak() => Console.WriteLine("メェー!");
}
#endregion

#region IDamageable と派生クラス(Enemy / Barrel) ---------------
interface IDamageable
{
    void TakeDamage(int amount);

    /* 【問題 B】Heal を追加 */
    void Heal(int amount);
}

class Enemy : IDamageable
{
    private int _hp = 100;

    public void TakeDamage(int amount)
    {
        _hp -= amount;
        Console.WriteLine($"敵に {amount} ダメージ! HP:{_hp}");
    }

    public void Heal(int amount)
    {
        _hp += amount;
        Console.WriteLine($"敵を {amount} 回復!     HP:{_hp}");
    }
}

class Barrel : IDamageable
{
    private int _durability = 50;

    public void TakeDamage(int amount)
    {
        _durability -= amount;
        Console.WriteLine($"タルに {amount} ダメージ! Durability:{_durability}");
    }

    public void Heal(int amount)
    {
        _durability += amount;
        Console.WriteLine($"タルを {amount} 修復!       Durability:{_durability}");
    }
}
#endregion

class Program
{
    static void Main()
    {
        //----------------------------------------------------------
        // 動物園デモ(Dog / Cat / Sheep が順に鳴く)
        //----------------------------------------------------------
        List<Animal> zoo = new()
        {
            new Dog(), new Cat(), new Sheep()          // ← Sheep を混在
        };

        Console.WriteLine("=== 動物園 ===");
        foreach (Animal a in zoo)
            a.Speak();

        //----------------------------------------------------------
        // 【問題 C】IDamageable リストでダメージ ↔ 回復を交互に実行
        //----------------------------------------------------------
        List<IDamageable> objects = new()
        {
            new Enemy(), new Barrel()
        };

        Console.WriteLine("\n=== テストコード ===");
        foreach (var obj in objects)
        {
            obj.TakeDamage(30);
            obj.Heal(10);
        }
    }
}

実行結果(例)

=== 動物園 ===
ワン!
ニャー!
メェー!

=== テストコード ===
敵に 30 ダメージ! HP:70
敵を 10 回復!     HP:80
タルに 30 ダメージ! Durability:20
タルを 10 修復!       Durability:30
  • 問題 A:Sheep は Animal を継承し、Speak() を「メェー!」にオーバーライド。
  • 問題 B:IDamageable に Heal() を追加し、Enemy・Barrel で実装。
  • 問題 C:List<IDamageable> に 2 つのオブジェクトを入れ、TakeDamage() と Heal() を交互に呼び出して挙動を確認します。

これで練習課題のサンプル解答は完了です。ぜひ自分でも数値を変えたりクラスを追加して動きを試してみてください。 


まとめ

  • ポリモーフィズム = “同じ操作で多様な結果”
  • 静的型が「呼べるメソッドを決め」
  • 動的型が「実際に動くメソッドを決める」
  • 基底クラスやインターフェースを“リモコン”に見立てると理解しやすい
  • 拡張・保守・テストが一気にラクになる —— 実戦でこそ真価が光る!

これでポリモーフィズムの入口はクリアです。次は自分のゲームオブジェクトや業務モデルに「どこで型をそろえ、どこで差を付けるか」を考えて設計してみましょう。

訪問数 2 回, 今日の訪問数 1回