閉じる

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

オブジェクト指向の要(かなめ)であるポリモーフィズムは、同じメソッド呼び出しでも実体(オブジェクト)に応じて動作を切り替える仕組みです。

本稿では、C#のvirtualoverrideを中心に、初学者がつまずきやすい点を押さえながら、実行可能なサンプルで丁寧に解説します。

基底型の参照で一括操作できることが最大のメリットです。

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

同じ呼び出しで動作を切り替える

ポリモーフィズムの直感

クラス階層で共通のメソッド名を使い回すと、呼び出し側は常に同じ名前で呼び出せますが、実際に呼ばれる処理はオブジェクトの実体ごとに変わります。

これがポリモーフィズム(多態性)です。

実行時バインディングのイメージ

C#ではvirtualなメンバーをoverrideした場合、実行時に動的ディスパッチが行われ、実体の型に合った実装が選ばれます

同じSpeak()でも、犬なら吠え、猫なら鳴くように切り替わります。

基底クラスと派生クラスの前提

継承の最低限

ポリモーフィズムを使うには、少なくとも1つの基底クラスと、それを継承した派生クラスが必要です。

基底クラスに共通のメソッドをvirtualで宣言し、派生クラスでoverrideします。

参照の代入関係

派生インスタンスは基底型の変数に代入できます。

基底型の参照で呼び出しても、実体の派生クラスの実装が動くのがポイントです。

どんな場面で役立つか

変更に強い設計

呼び出し側は共通インターフェースに依存し、実装詳細を知りません。

新しい派生クラスを追加しても呼び出し側をほぼ変更せずに拡張できます。

テスト容易性

テスト時にダミーの派生クラスを差し替えられるため、外部依存を切り離した検証が行いやすくなります。

virtual/override の基本

virtual(上書き可能)の意味

宣言場所と効果

virtualは基底クラス側に付けます。

これにより「このメンバーは派生クラスで差し替え可能」であることを示します。

メソッドだけでなくプロパティやインデクサーにも使えます。

override(上書き)の書き方

基本構文

派生クラスでoverrideを付けて実装すると、基底のvirtualを上書きします。

戻り値や引数は一致させます。

シグネチャは一致させる

同じメソッド名・引数・戻り値

オーバーライドはシグネチャ一致が必須です。

アクセス修飾子の縮小(例: publicprotectedに)はできません。

基底型の参照で効果が出る

Listでまとめて扱う

基底型のコレクションを通じて一括操作する時に真価を発揮します。

これにより呼び出し側コードがシンプルで保守性が高くなります。

new と override の違い(注意点)

メンバーの隠蔽 vs 真のオーバーライド

newは同名メンバーを「隠す」だけで、基底型の参照からは基底の実装が呼ばれます。

振る舞いの差し替えにはoverrideを使います

以下は簡単な比較です。

目的キーワード呼び出し解決基底型参照からの呼び出し
振る舞いの差し替えoverride実行時(動的ディスパッチ)派生の実装が呼ばれる
名前の隠蔽(独立した別メンバー)newコンパイル時基底の実装が呼ばれる

プロパティの virtual/override

get/set の扱い

プロパティにもvirtual/overrideは適用できます。

オーバーライドではアクセサ(get/set)構成を変えられません

基底がgetのみなら、派生でもgetのみです。

例:

C#
// プロパティのvirtual/override例
public class Person
{
    // DisplayNameは派生で加工できるようにvirtual
    public virtual string DisplayName { get; set; } = "Unknown";
}

public class Guest : Person
{
    // getで表示名に注釈をつけ、setは基底の動作を再利用
    public override string DisplayName
    {
        get => base.DisplayName + " (Guest)";
        set => base.DisplayName = value; // 必要なら検証ロジックを加える
    }
}

サンプルで理解(Animal.Speak)

基底クラスに virtual メソッドを定義

まずは共通の骨組み

Animalvirtualなメソッドとプロパティを持たせます。

プロパティSoundSpeak()で使うことで、プロパティのオーバーライド効果も合わせて確認できます。

派生クラスで override して振る舞いを変更

Dog/Cat で鳴き声を差し替え

派生クラスはSoundoverrideします。

メソッドもプロパティも差し替え可能という点に注目してください。

基底型の List で一括呼び出し

実行するプログラム全体

C#
using System;
using System.Collections.Generic;

// 基底クラス(上書き可能なメソッドとプロパティ)
public class Animal
{
    // 鳴き声(派生で上書き可能)
    public virtual string Sound => "(silence)";

    // 話す(派生で上書き可能)
    public virtual void Speak()
    {
        // GetType().Name で実体の型名を表示
        Console.WriteLine($"{GetType().Name} says: {Sound}");
    }
}

// 派生クラス: Dog
public class Dog : Animal
{
    public override string Sound => "Woof";
    public override void Speak()
    {
        // 必要なら独自の前処理を追加
        Console.WriteLine($"{GetType().Name} says: {Sound}!");
    }
}

// 派生クラス: Cat
public class Cat : Animal
{
    public override string Sound => "Meow";
    // Speak() を上書きしない場合、基底のSpeak()が使われる
}

public class Program
{
    public static void Main()
    {
        // 基底型(Animal)のリストに、さまざまな実体を入れる
        var animals = new List<Animal>
        {
            new Animal(),
            new Dog(),
            new Cat()
        };

        // 同じ呼び出しでも、実体に応じて動作が切り替わる
        foreach (var a in animals)
        {
            a.Speak(); // ポリモーフィズムの要点
        }
    }
}
実行結果
Animal says: (silence)
Dog says: Woof!
Cat says: Meow

実行結果のイメージ

Listに入っているのはすべてAnimal型の参照ですが、実行時には実体(Dog/Cat)の実装が選ばれています。

これがvirtual/overrideによる動的ディスパッチです。

初心者の落とし穴とコツ

override し忘れをコンパイラで防ぐ

差し替え必須のメンバーは基底をabstractにするのが有効です。

抽象メソッドは派生でoverrideしないとコンパイルエラーになります。

さらに、これ以上の上書きを禁じたい場合はsealed overrideを使います。

不要な override は作らない

動作を変えないのに形だけoverrideすると、読み手を混乱させます。

基底の挙動をそのまま使うなら、上書き自体を作らないか、設計を見直します。

共通の基底クラスを決めておく

あらかじめ「この系統はこの基底から継承する」という前提をチーム内で共有すると、基底型の参照で統一的に扱えるため、コレクション操作やDI(依存性の注入)がスムーズになります。

インターフェースIAnimalを併用する設計も有効です。

よくある例(ToString の override)

ToString()はデバッグやログで頻繁に表示されるため、適切にoverrideしておくととても便利です。

C#
using System;

// 表示用にToString()を上書き
public class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // オブジェクトの要約表示
    public override string ToString()
    {
        return $"Point(X={X}, Y={Y})";
    }
}

public class Program
{
    public static void Main()
    {
        var p = new Point(3, 4);
        Console.WriteLine(p);        // Console.WriteLineは内部でToString()を呼ぶ
        Console.WriteLine(p.ToString());
    }
}
実行結果
Point(X=3, Y=4)
Point(X=3, Y=4)

new と override の違いを動作で確認

間違ってnewを使うと、基底型参照からは基底の実装が動くことを確認しておきましょう。

C#
using System;

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal says: (silence)");
    }
}

public class Bird : Animal
{
    // 隠蔽(new) — オーバーライドではない
    public new void Speak()
    {
        Console.WriteLine("Bird chirps");
    }
}

public class Program
{
    public static void Main()
    {
        Animal a = new Bird();
        a.Speak();         // 基底の実装が呼ばれる

        Bird b = new Bird();
        b.Speak();         // 隠蔽したBirdの実装が呼ばれる
    }
}
実行結果
Animal says: (silence)
Bird chirps

動作の差し替え目的ならoverride一択だと覚えておくと安全です。

まとめ

ポリモーフィズムは、同じ呼び出しで実体に応じて動作を切り替える仕組みで、拡張しやすく保守性の高い設計に直結します。

C#では基底にvirtual、派生にoverrideを用い、基底型の参照から呼び出すことで効果が現れます。

newは隠蔽であり差し替えではない点に注意しつつ、必要最低限のoverrideにとどめ、ToString()などの実用的な上書きから練習すると理解が深まります。

サンプルを写経して、実行結果と照らし合わせながら確実に身につけていきましょう。

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

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

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

URLをコピーしました!