閉じる

C#で学ぶポリモーフィズム(virtual/override)入門

C#のポリモーフィズム(多態)は、同じ呼び出しでも実体に応じて動作を切り替える考え方です。

本記事ではvirtual/overrideを中心に、基礎から実践、よくある間違いまでをサンプルと実行結果で段階的に学びます。

必要な範囲でクラスやプロパティ、継承もあわせて確認します。

ポリモーフィズムとは?C#の基本

ポリモーフィズムの定義とメリット

ポリモーフィズム(多態)とは、同じメソッド呼び出しであっても、実際のインスタンスの型ごとに異なる実装が呼ばれる仕組みです。

C#では主に継承とvirtual/overrideによる動的ディスパッチで実現します。

これにより呼び出し側は共通の型(基底クラス)だけを意識すればよく、実装の差し替えや拡張が容易になります。

  • メリットは、拡張容易性(変更に強い)、テスト容易性(モック差し替え)、読みやすさ(共通の呼び出し口)です。
  • C#ではデフォルトでメソッドは非仮想(非virtual)です。多態を効かせるには基底側にvirtual、派生側にoverrideを明示します。

具体例(イメージ)

例えばAnimalという基底クラスにSpeak()があり、DogCatがそれぞれの鳴き声を実装すると、Animal型の配列に混在させてSpeak()を呼ぶだけで、個々の鳴き声が出力されます。

基底クラスと派生クラスの関係

C#ではclass Dog : Animalのようにコロンで継承を表します。

派生クラスは基底クラスのメンバーを引き継ぎ、必要に応じてvirtualなメソッドをoverrideで上書きします。

プロパティ({ get; set; })は共通の状態(例: 名前)を扱うのに便利です。

クラス、インスタンス、プロパティの最小例

C#
using System;

public class Animal
{
    // 自動実装プロパティ(状態を表す)
    public string Name { get; set; }

    // コンストラクタで初期化できるようにする
    public Animal() { }
    public Animal(string name) { Name = name; }

    // 基底の振る舞い(ここでは仮想にしていません)
    public void Info()
    {
        Console.WriteLine($"これは動物です。名前は{Name}です。");
    }
}

public class Dog : Animal // Animalを継承
{
    // 独自のコンストラクタ(基底のNameを設定)
    public Dog(string name)
    {
        Name = name;
    }
}

public class Program
{
    public static void Main()
    {
        // インスタンス(オブジェクト)の生成
        Animal a = new Animal("ななし");
        Dog d = new Dog("ポチ");

        // プロパティ参照とメソッド呼び出し
        a.Info();
        d.Info();
    }
}
実行結果
これは動物です。名前はななしです。
これは動物です。名前はポチです。

virtual/overrideが必要な理由

非virtualなメソッドはコンパイル時の型で解決されます。

一方、virtualを付けたメソッドは実行時の実体(派生型)に応じて解決されます。

C#では安全性と明確さのため、明示的にvirtual/overrideを使って多態を有効化します。

virtualで上書き可能にする

virtualメソッドの書き方

基底クラスで上書き可能にしたいメソッドにvirtualを付けます。

サンプル(AnimalとDogで鳴き声を多態化)

C#
using System;

public class Animal
{
    public string Name { get; set; }
    public Animal() { }
    public Animal(string name) { Name = name; }

    // 上書き可能にする
    public virtual void Speak()
    {
        Console.WriteLine($"動物({Name})が鳴きます。");
    }
}

public class Dog : Animal
{
    public Dog(string name) { Name = name; }

    // overrideで実際の動作を上書き
    public override void Speak()
    {
        Console.WriteLine($"犬({Name})がワンと鳴きます。");
    }
}

public class Program
{
    public static void Main()
    {
        Animal a = new Animal("ななし");
        Animal d = new Dog("ポチ"); // 変数型はAnimal、実体はDog

        a.Speak();
        d.Speak(); // 実体がDogなのでDog版が呼ばれる
    }
}
実行結果
動物(ななし)が鳴きます。
犬(ポチ)がワンと鳴きます。

非virtualメソッドとの違い

非virtualメソッドは上書き(override)できません。

間違ってoverrideを付けるとコンパイルエラーになります。

オーバーライド不可の例

C#
public class Animal
{
    public void Move() // 非virtual
    {
        Console.WriteLine("動物が歩きます。");
    }
}

public class Dog : Animal
{
    // エラー: 'Dog.Move()' は 'Animal.Move()' をオーバーライドできません
    // public override void Move() { ... }
}

上のような場合、どうしても派生で同名メソッドを定義したいならnewによるメンバーの隠蔽(後述)になりますが、これは多態ではありません。

どのメソッドをvirtualにするか

むやみにvirtualを付けると設計が複雑になります。

判断の目安としては、拡張ポイントとして意図的に差し替えたい振る舞い、ドメイン上で「種類によって異なる」ことが明確な振る舞いに限定します。

基底で振る舞いを固定したいメソッドは非virtualのままにします。

さらに、コンストラクタや初期化フローの中で呼ぶ可能性があるメソッドは、virtualにすると危険が増えるため慎重に検討します。

overrideで派生クラスの動作を上書き

overrideメソッドの書き方

派生クラス側ではoverrideを付けて基底のvirtualメソッドを上書きします。

戻り値、メソッド名、引数、修飾子の整合性(シグネチャ)が重要です。

基本構文

C#
public class Base
{
    public virtual int Calc(int x) => x;
}

public class Derived : Base
{
    public override int Calc(int x) // シグネチャ一致が必須
    {
        return x * 2;
    }
}

基底クラス型での呼び出し例

同じ呼び出しでも実体ごとに動作が切り替わる様子を確認します。

配列での多態呼び出し

C#
using System;

public class Animal
{
    public string Name { get; set; }
    public Animal(string name) { Name = name; }
    public virtual void Speak() => Console.WriteLine($"動物({Name})が鳴きます。");
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override void Speak() => Console.WriteLine($"犬({Name})がワンと鳴きます。");
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void Speak() => Console.WriteLine($"猫({Name})がニャーと鳴きます。");
}

public class Program
{
    public static void Main()
    {
        Animal[] zoo =
        {
            new Animal("ななし"),
            new Dog("ポチ"),
            new Cat("タマ")
        };

        foreach (var a in zoo)
        {
            a.Speak(); // 実体に応じてそれぞれの実装が呼ばれる
        }
    }
}
実行結果
動物(ななし)が鳴きます。
犬(ポチ)がワンと鳴きます。
猫(タマ)がニャーと鳴きます。

overrideとnewの違い

overrideは多態(動的ディスパッチ)を有効にします。

newは「同名メンバーの隠蔽」で、変数の静的型によってどちらが呼ばれるかが決まります。

newは多態ではありません。

比較用コード(virtual/override と new)

C#
using System;

public class Animal
{
    public string Name { get; set; }
    public Animal(string name) { Name = name; }

    public virtual void Speak() // 多態の対象
        => Console.WriteLine($"動物({Name})が鳴きます。");

    public void Move() // 非virtual
        => Console.WriteLine($"動物({Name})が歩きます。");
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void Speak() // 多態: 実体に応じて呼ばれる
        => Console.WriteLine($"犬({Name})がワンと鳴きます。");

    public new void Move() // メンバーの隠蔽: 変数の型で決まる
        => Console.WriteLine($"犬({Name})が走ります。");
}

public class Program
{
    public static void Main()
    {
        Animal a = new Dog("ポチ"); // 変数型はAnimal、実体はDog
        Dog d = new Dog("ポチ");

        a.Speak(); // override版(Dog)が呼ばれる
        d.Speak(); // override版(Dog)が呼ばれる

        a.Move();  // 変数の静的型はAnimal → Animal.Move()
        d.Move();  // 変数の静的型はDog    → Dog.Move()
    }
}
実行結果
犬(ポチ)がワンと鳴きます。
犬(ポチ)がワンと鳴きます。
動物(ポチ)が歩きます。
犬(ポチ)が走ります。

違いの要点を表にまとめます。

キーワード多態(動的ディスパッチ)呼び出しの決定要因用途
virtual/overrideあり実行時の実体基底の拡張ポイントを派生で差し替える
new (隠蔽)なし変数の静的型同名を定義したいが上書きはしないケース

newは意図が明確なときのみ使い、基本はvirtual/overrideで設計するのが安全です。

よくある間違いとチェックリスト

virtualを付け忘れて多態が働かない

基底が非virtualのままだと、派生でoverrideできません。

以下はコンパイルエラーになります。

C#
public class Animal
{
    public void Speak() { /* 非virtual */ }
}

public class Dog : Animal
{
    // エラー: 'Dog.Speak()' は 'Animal.Speak()' をオーバーライドできません (CS0506)
    // public override void Speak() { }
}

また、基底がvirtualでも、派生側でoverrideを付け忘れると、既存のメソッドを「隠蔽」してしまい、多態が働かなくなります(警告CS0114)。

次の例は警告ののち、実行時に意図通りに切り替わりません。

C#
using System;

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

public class Dog : Animal
{
    // 本来は override を付けるべきだが付けていない(隠蔽になる)
    public void Speak() => Console.WriteLine("犬がワンと鳴きます。");
}

public class Program
{
    public static void Main()
    {
        Animal a = new Dog();
        a.Speak(); // 基底のvirtual版が呼ばれてしまう
    }
}
実行結果
動物が鳴きます。

対応策は、基底にvirtual、派生に必ずoverrideを付けることです。

シグネチャ不一致でoverrideになっていない

overrideはシグネチャ(戻り値、名前、引数、修飾子)が完全一致している必要があります。

引数や戻り値が違うとコンパイルエラー、または別メソッド扱いになります。

C#
public class Animal
{
    public virtual void Speak(string mood) { }
}

public class Dog : Animal
{
    // エラー: 一致する仮想メンバーが存在しない (引数が違う)
    // public override void Speak() { }

    // 正しい override
    public override void Speak(string mood) { }
}

デフォルト引数やref/out/inなどの違いも一致要件に含まれます。

意図しない隠蔽や別メソッド化を避けるため、IDEの補完でoverrideの候補から生成するのが確実です。

コンストラクタ内のvirtual呼び出しに注意

コンストラクタ内でvirtualメソッドを呼ぶと、派生のoverrideが「派生のコンストラクタが走る前」に呼ばれます。

未初期化の状態で派生の処理が動作し、予期しない振る舞いになります。

望ましくない例

C#
using System;

public class Base
{
    public int Value;

    public Base()
    {
        Init(); // virtual呼び出し(危険)
    }

    public virtual void Init()
    {
        Console.WriteLine($"Base.Init: Value={Value}");
    }
}

public class Derived : Base
{
    public Derived()
    {
        Value = 10; // ここはまだ呼ばれていないタイミングで override が動く
        Console.WriteLine("Derived コンストラクタ完了");
    }

    public override void Init()
    {
        Console.WriteLine($"Derived.Init(override): Value={Value}");
    }
}

public class Program
{
    public static void Main()
    {
        var d = new Derived();
        Console.WriteLine($"最後の値: {d.Value}");
    }
}
実行結果
Derived.Init(override): Value=0
Derived コンストラクタ完了
最後の値: 10

Init()が派生側で呼ばれた時点ではValueがまだ0で、派生の初期化が完了していません。

改善例としては、コンストラクタ内では virtual メソッドを呼ばず、代わりに明示的な初期化メソッドを呼び出す責務を持たせる方法が推奨されます。これにより、派生クラスの未初期化状態で override が走る問題を防げます。

C#
using System;

public class Base
{
    public int Value;

    public Base()
    {
        // コンストラクタでは直接Initを呼ばない
    }

    public void Initialize()
    {
        Init();
    }

    protected virtual void Init()
    {
        Console.WriteLine($"Base.Init: Value={Value}");
    }
}

public class Derived : Base
{
    public Derived()
    {
        Value = 10;
        Console.WriteLine("Derived コンストラクタ完了");
        Initialize(); // ここで明示的に呼び出す
    }

    protected override void Init()
    {
        Console.WriteLine($"Derived.Init(override): Value={Value}");
    }
}

public class Program
{
    public static void Main()
    {
        var d = new Derived();
        Console.WriteLine($"最後の値: {d.Value}");
    }
}
実行結果
Derived コンストラクタ完了
Derived.Init(override): Value=10
最後の値: 10

コンストラクタ内で virtual メソッドを呼ぶと、派生クラス側のオーバーライドが未初期化状態で実行され、予期せぬ動作を招く危険があります。

改善例では、基底クラスに明示的な初期化メソッドを設け、派生クラスのコンストラクタが完了した後に呼ぶことで安全にオーバーライドを利用できます。

まとめ

本記事では、C#のポリモーフィズムをvirtual/overrideに焦点を当てて解説しました。

ポイントは次のとおりです。

基底でvirtualを宣言し、派生でoverrideすることで、基底型越しの呼び出しでも実体に応じた実装が選ばれます。

newは同名メンバーの隠蔽であり多態ではないため、設計上の意図があるときのみ使用します。

また、overrideのシグネチャ一致や、コンストラクタ内のvirtual呼び出しの危険性など、つまずきやすい点に注意することで、拡張に強く読みやすいオブジェクト指向設計を実現できます。

実プロジェクトでは、差し替えたい振る舞いに絞ってvirtualを導入し、テストや将来の拡張まで見据えたクラス設計を心がけてください。

この記事を書いた人
エーテリア編集部
エーテリア編集部

C#の入門記事を中心に、開発環境の準備からオブジェクト指向の基本まで、順を追って解説しています。ゲーム開発や業務アプリを目指す人にも役立ちます。

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

URLをコピーしました!