閉じる

【C#】virtualとoverrideで理解するポリモーフィズム入門

オブジェクト指向の中核となる考え方の1つが「ポリモーフィズム」です。

C#ではvirtualとoverrideを使うことで、このポリモーフィズムをシンプルかつ強力に表現できます。

本記事では、継承の基本から始めて、virtualとoverrideの役割、実行時の動き、ありがちなつまずきポイントまでを、図解とサンプルコードを使って丁寧に解説していきます。

C#におけるポリモーフィズムとは

ポリモーフィズムの基本イメージ

ポリモーフィズムとは、「同じメソッド呼び出しであっても、実際のオブジェクトの型によって振る舞いが変わる」という仕組みです。

C#では特に、継承関係にあるクラス同士でこれを実現します。

たとえば、AnimalクラスからDogやCatが派生して、同じメソッド名Speakでも、Dogなら「ワン」、Catなら「ニャー」と鳴くといったイメージです。

このような「呼ぶ側はAnimalとして扱うが、実際の動作はDogやCatごとに変わる」という挙動を、C#ではvirtualとoverrideで実現します。

なぜvirtualとoverrideが必要なのか

C#では、単に同じ名前のメソッドを派生クラスに定義しただけでは、ポリモーフィズムは正しく動作しません。

「基底クラス側でvirtualを付ける」「派生クラス側でoverrideを付ける」ことで、初めて動的なメソッドの切り替え(動的ディスパッチ)が有効になります。

virtualとoverrideの基本

virtualメソッドとは

基底クラス側でvirtualを付けると、そのメソッドは「派生クラスで動作を上書きしてよい」という宣言になります。

これにより、派生クラスはoverrideを使って動作を変更できるようになります。

overrideメソッドとは

派生クラス側でoverrideを付けると、「基底クラスのvirtualメソッドの実装を上書きする」ことを意味します。

このとき、メソッドのシグネチャ(戻り値の型、メソッド名、引数リスト)は基底クラスと一致している必要があります。

まずはシンプルなサンプルコード

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

以下に、Animalを基底クラスとしたシンプルな例を示します。

C#
using System;

// 動物を表す基底クラス
class Animal
{
    // virtual: 派生クラスでオーバーライド可能なメソッド
    public virtual void Speak()
    {
        Console.WriteLine("Animal: 何かの鳴き声");
    }
}

// イヌを表す派生クラス
class Dog : Animal
{
    // override: 基底クラスAnimalのSpeakを上書き
    public override void Speak()
    {
        Console.WriteLine("Dog: ワンワン!");
    }
}

// ネコを表す派生クラス
class Cat : Animal
{
    // override: 基底クラスAnimalのSpeakを上書き
    public override void Speak()
    {
        Console.WriteLine("Cat: ニャーニャー!");
    }
}

class Program
{
    static void Main()
    {
        // すべて「Animal型」の変数に代入している点がポイント
        Animal a1 = new Animal();
        Animal a2 = new Dog();
        Animal a3 = new Cat();

        // 同じSpeakメソッド呼び出しでも、実際の型ごとに動作が変わる
        a1.Speak(); // AnimalのSpeakが呼ばれる
        a2.Speak(); // DogのSpeakが呼ばれる
        a3.Speak(); // CatのSpeakが呼ばれる
    }
}
実行結果
Animal: 何かの鳴き声
Dog: ワンワン!
Cat: ニャーニャー!

この例では、変数の型はいずれもAnimalですが、実際に保持しているインスタンスの型に応じて、呼び出されるSpeakの実装が自動的に切り替わっています

これがvirtualとoverrideによるポリモーフィズムです。

コンパイル時の型と実行時の型

型の2つの顔

C#の参照型の変数には、次の2つの観点があります。

  • コンパイル時の型… 変数宣言時に書かれている型(例: Animal)
  • 実行時の型… 実際にnewされた型(例: Dog, Cat)

virtual/overrideを使うと、コンパイル時の型ではなく、実行時の型に応じてメソッドが選ばれます

そのため、Animal型のリストにDogやCatを混在させても、ループの中で同じSpeak()を呼ぶだけで、それぞれ適切な鳴き声になります。

Listでまとめて扱う例

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

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal: 何かの鳴き声");
    }
}

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

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

class Program
{
    static void Main()
    {
        // Animal型のリストに、さまざまな動物を格納
        var animals = new List<Animal>
        {
            new Animal(),
            new Dog(),
            new Cat(),
            new Dog()
        };

        foreach (var animal in animals)
        {
            // 変数型は常にAnimalだが、実行時の型ごとに動作が変わる
            animal.Speak();
        }
    }
}
実行結果
Animal: 何かの鳴き声
Dog: ワンワン!
Cat: ニャーニャー!
Dog: ワンワン!

「呼ぶ側は共通の型(Animal)だけを意識し、実際の振る舞いは各クラスに任せる」ことで、拡張しやすく保守しやすい設計になります。

baseを使って基底クラスの実装も利用する

overrideしつつ、元の処理も使いたい場合

派生クラスでoverrideするとき、baseキーワードを使って基底クラスの実装を呼び出すこともできます。

これにより、共通部分を基底クラスにまとめ、派生クラスでは追加の処理だけを書く、といった設計が可能になります。

C#
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal: (共通の前処理) 何かの鳴き声");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        // まず基底クラスの共通処理を実行
        base.Speak();

        // そのあとでDog独自の処理を追加
        Console.WriteLine("Dog: ワンワン!(イヌ独自の鳴き声)");
    }
}

class Program
{
    static void Main()
    {
        Animal animal = new Dog();
        animal.Speak();
    }
}
実行結果
Animal: (共通の前処理) 何かの鳴き声
Dog: ワンワン!(イヌ独自の鳴き声)

このように、基底クラスで共通の手順を用意し、派生クラスで差分だけを追加する設計は実務でもよく使われます。

newとの違いに注意しよう

overrideではなくnewを使うとどうなるか

C#では、同名メソッドを定義するときにoverrideの代わりにnewを使うこともできますが、これはポリモーフィズムではありません

new「基底クラスのメソッドを隠す(メソッド隠蔽)」だけで、実行時の型に基づく動的な切り替えは行いません。

C#
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal: 何かの鳴き声");
    }
}

class Dog : Animal
{
    // overrideではなくnewを使った例
    public new void Speak()
    {
        Console.WriteLine("Dog(new): ワンワン!");
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Dog(); // 実行時の型はDog
        Dog d = new Dog();

        a.Speak(); // どちらのSpeakが呼ばれるか?
        d.Speak();
    }
}
実行結果
Animal: 何かの鳴き声
Dog(new): ワンワン!

この結果からわかるように、変数の型がAnimalである場合、newで隠されたDog側のSpeakは呼ばれません

ポリモーフィズムを実現したいときは、必ずvirtualとoverrideを組み合わせることが重要です。

よくあるつまずきポイント

virtualを付け忘れる

基底クラスにvirtualがないと、派生クラスでoverrideは使えません。

ついvirtualを付け忘れてエラーになる、というのは非常によくあるパターンです。

C#
class Animal
{
    // virtualを付け忘れた例
    public void Speak()
    {
        Console.WriteLine("Animal");
    }
}

class Dog : Animal
{
    // これはコンパイルエラーになる
    // public override void Speak() { ... }
}

この場合は基底クラス側にvirtualを付けるか、設計として本当にポリモーフィズムが必要なのかを見直します。

sealed overrideでさらに継承を止める

すでにoverrideされたメソッドを、さらに派生クラスで上書きされたくない場合は、sealed overrideという形にできます。

C#
class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal");
    }
}

class Dog : Animal
{
    // これ以上の派生クラスでSpeakをoverrideさせない
    public sealed override void Speak()
    {
        Console.WriteLine("Dog");
    }
}

このようにvirtual → override → sealed overrideという流れで、継承チェーンにおける拡張の余地をコントロールできます。

実務での活用イメージ

共通インターフェースとしての基底クラス

実務の場では、たとえば次のような場面でvirtual/overrideが活躍します。

  • 複数種類のログ出力(コンソール、ファイル、データベース)をLoggerという共通の基底クラスで扱う
  • 複数種類のUIコントロール(ボタン、テキストボックスなど)を共通のControlクラスから派生させる
  • 各種計算アルゴリズムを共通のCalculatorクラスで抽象化する

「共通の型でまとめて扱えるが、実際の処理は各クラスごとに異なる」、という構造を取ることで、ビジネスロジックの追加や変更を行いやすくなります。

まとめ

virtualとoverrideは、C#でポリモーフィズムを実現するための中核的な仕組みです。

基底クラスでvirtualを宣言し、派生クラスでoverrideすることで、コンパイル時には共通の型で扱いつつ、実行時には実際のオブジェクトごとに振る舞いを切り替えられます

newはメソッドを隠すだけで、ポリモーフィズムにはならない点にも注意が必要です。

この記事で紹介した基本パターンを押さえておけば、実務のクラス設計においても柔軟で拡張しやすいコードを書けるようになります。

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

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

URLをコピーしました!