C#のポリモーフィズム(多態)は、同じ呼び出しでも実体に応じて動作を切り替える考え方です。
本記事ではvirtual/overrideを中心に、基礎から実践、よくある間違いまでをサンプルと実行結果で段階的に学びます。
必要な範囲でクラスやプロパティ、継承もあわせて確認します。
ポリモーフィズムとは?C#の基本
ポリモーフィズムの定義とメリット
ポリモーフィズム(多態)とは、同じメソッド呼び出しであっても、実際のインスタンスの型ごとに異なる実装が呼ばれる仕組みです。
C#では主に継承とvirtual
/override
による動的ディスパッチで実現します。
これにより呼び出し側は共通の型(基底クラス)だけを意識すればよく、実装の差し替えや拡張が容易になります。
- メリットは、拡張容易性(変更に強い)、テスト容易性(モック差し替え)、読みやすさ(共通の呼び出し口)です。
- C#ではデフォルトでメソッドは非仮想(非virtual)です。多態を効かせるには基底側に
virtual
、派生側にoverride
を明示します。
具体例(イメージ)
例えばAnimal
という基底クラスにSpeak()
があり、Dog
やCat
がそれぞれの鳴き声を実装すると、Animal
型の配列に混在させてSpeak()
を呼ぶだけで、個々の鳴き声が出力されます。
基底クラスと派生クラスの関係
C#ではclass Dog : Animal
のようにコロンで継承を表します。
派生クラスは基底クラスのメンバーを引き継ぎ、必要に応じてvirtual
なメソッドをoverride
で上書きします。
プロパティ({ get; set; }
)は共通の状態(例: 名前)を扱うのに便利です。
クラス、インスタンス、プロパティの最小例
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で鳴き声を多態化)
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
を付けるとコンパイルエラーになります。
オーバーライド不可の例
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
メソッドを上書きします。
戻り値、メソッド名、引数、修飾子の整合性(シグネチャ)が重要です。
基本構文
public class Base
{
public virtual int Calc(int x) => x;
}
public class Derived : Base
{
public override int Calc(int x) // シグネチャ一致が必須
{
return x * 2;
}
}
基底クラス型での呼び出し例
同じ呼び出しでも実体ごとに動作が切り替わる様子を確認します。
配列での多態呼び出し
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)
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
できません。
以下はコンパイルエラーになります。
public class Animal
{
public void Speak() { /* 非virtual */ }
}
public class Dog : Animal
{
// エラー: 'Dog.Speak()' は 'Animal.Speak()' をオーバーライドできません (CS0506)
// public override void Speak() { }
}
また、基底がvirtualでも、派生側でoverride
を付け忘れると、既存のメソッドを「隠蔽」してしまい、多態が働かなくなります(警告CS0114)。
次の例は警告ののち、実行時に意図通りに切り替わりません。
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
はシグネチャ(戻り値、名前、引数、修飾子)が完全一致している必要があります。
引数や戻り値が違うとコンパイルエラー、または別メソッド扱いになります。
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
が「派生のコンストラクタが走る前」に呼ばれます。
未初期化の状態で派生の処理が動作し、予期しない振る舞いになります。
望ましくない例
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
が走る問題を防げます。
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
を導入し、テストや将来の拡張まで見据えたクラス設計を心がけてください。