オブジェクト指向の要(かなめ)であるポリモーフィズムは、同じメソッド呼び出しでも実体(オブジェクト)に応じて動作を切り替える仕組みです。
本稿では、C#のvirtual
とoverride
を中心に、初学者がつまずきやすい点を押さえながら、実行可能なサンプルで丁寧に解説します。
基底型の参照で一括操作できることが最大のメリットです。
C#のポリモーフィズムとは
同じ呼び出しで動作を切り替える
ポリモーフィズムの直感
クラス階層で共通のメソッド名を使い回すと、呼び出し側は常に同じ名前で呼び出せますが、実際に呼ばれる処理はオブジェクトの実体ごとに変わります。
これがポリモーフィズム(多態性)です。
実行時バインディングのイメージ
C#ではvirtual
なメンバーをoverride
した場合、実行時に動的ディスパッチが行われ、実体の型に合った実装が選ばれます。
同じSpeak()
でも、犬なら吠え、猫なら鳴くように切り替わります。
基底クラスと派生クラスの前提
継承の最低限
ポリモーフィズムを使うには、少なくとも1つの基底クラスと、それを継承した派生クラスが必要です。
基底クラスに共通のメソッドをvirtual
で宣言し、派生クラスでoverride
します。
参照の代入関係
派生インスタンスは基底型の変数に代入できます。
基底型の参照で呼び出しても、実体の派生クラスの実装が動くのがポイントです。
どんな場面で役立つか
変更に強い設計
呼び出し側は共通インターフェースに依存し、実装詳細を知りません。
新しい派生クラスを追加しても呼び出し側をほぼ変更せずに拡張できます。
テスト容易性
テスト時にダミーの派生クラスを差し替えられるため、外部依存を切り離した検証が行いやすくなります。
virtual/override の基本
virtual(上書き可能)の意味
宣言場所と効果
virtual
は基底クラス側に付けます。
これにより「このメンバーは派生クラスで差し替え可能」であることを示します。
メソッドだけでなくプロパティやインデクサーにも使えます。
override(上書き)の書き方
基本構文
派生クラスでoverride
を付けて実装すると、基底のvirtual
を上書きします。
戻り値や引数は一致させます。
シグネチャは一致させる
同じメソッド名・引数・戻り値
オーバーライドはシグネチャ一致が必須です。
アクセス修飾子の縮小(例: public
をprotected
に)はできません。
基底型の参照で効果が出る
Listでまとめて扱う
基底型のコレクションを通じて一括操作する時に真価を発揮します。
これにより呼び出し側コードがシンプルで保守性が高くなります。
new と override の違い(注意点)
メンバーの隠蔽 vs 真のオーバーライド
new
は同名メンバーを「隠す」だけで、基底型の参照からは基底の実装が呼ばれます。
振る舞いの差し替えにはoverride
を使います。
以下は簡単な比較です。
目的 | キーワード | 呼び出し解決 | 基底型参照からの呼び出し |
---|---|---|---|
振る舞いの差し替え | override | 実行時(動的ディスパッチ) | 派生の実装が呼ばれる |
名前の隠蔽(独立した別メンバー) | new | コンパイル時 | 基底の実装が呼ばれる |
プロパティの virtual/override
get/set の扱い
プロパティにもvirtual
/override
は適用できます。
オーバーライドではアクセサ(get/set)構成を変えられません。
基底がgetのみなら、派生でもgetのみです。
例:
// プロパティの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 メソッドを定義
まずは共通の骨組み
Animal
にvirtual
なメソッドとプロパティを持たせます。
プロパティSound
をSpeak()
で使うことで、プロパティのオーバーライド効果も合わせて確認できます。
派生クラスで override して振る舞いを変更
Dog/Cat で鳴き声を差し替え
派生クラスはSound
をoverride
します。
メソッドもプロパティも差し替え可能という点に注目してください。
基底型の List で一括呼び出し
実行するプログラム全体
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
しておくととても便利です。
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
を使うと、基底型参照からは基底の実装が動くことを確認しておきましょう。
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()
などの実用的な上書きから練習すると理解が深まります。
サンプルを写経して、実行結果と照らし合わせながら確実に身につけていきましょう。