C#は強力なオブジェクト指向プログラミング言語であり、その中心的な概念の一つに「継承」があります。
継承を利用することで、既存のクラスの機能を再利用しつつ、新しい機能を追加したり、既存の振る舞いを作り替えたりすることが可能です。
この「既存の振る舞いを作り替える」仕組みこそがオーバーライド (Override)です。
オーバーライドを正しく理解し、virtualやabstractといったキーワードを使いこなすことは、保守性が高く柔軟なプログラムを設計する上で避けては通れません。
本記事では、C#におけるオーバーライドの基礎から、実務で役立つ応用的なテクニック、そして間違いやすい「メソッド隠蔽 (new)」との違いまで、エンジニアとして知っておくべき知識を網羅的に解説します。
C#におけるオーバーライドの基本概念
オーバーライドとは、親クラス (基底クラス) で定義されたメソッドを、子クラス (派生クラス) で再定義することを指します。
これにより、同じメソッド名でありながら、呼び出し元のオブジェクトの型に応じて異なる処理を実行させる「ポリモーフィズム (多態性)」を実現できます。
オブジェクト指向設計において、共通のインターフェースを持たせつつ、個別のクラスごとに最適な動作を実装したい場合にオーバーライドは非常に有効です。
例えば、「動物」という基底クラスに「鳴く」というメソッドがある場合、それを継承した「犬」クラスでは「ワンワン」、「猫」クラスでは「ニャー」と鳴くように動作を上書きすることができます。
C#でオーバーライドを実現するためには、基底クラス側でそのメソッドが上書き可能であることを明示し、派生クラス側で上書きすることを明示する必要があります。
これに使用するのがvirtualキーワードとoverrideキーワードです。
virtualキーワードとoverrideキーワードの使い方
C#では、デフォルトの状態ではメソッドをオーバーライドすることはできません。
オーバーライドを許可するためには、基底クラスのメソッドにvirtual修飾子を付与する必要があります。
基本的な実装手順
まずは、最もシンプルなオーバーライドの例を見てみましょう。
以下のプログラムでは、AnimalクラスのメソッドをDogクラスで上書きしています。
using System;
namespace OverrideExample
{
// 基底クラス (親クラス)
public class Animal
{
// virtualキーワードにより、派生クラスでのオーバーライドを許可する
public virtual void MakeSound()
{
Console.WriteLine("動物が音を出します。");
}
}
// 派生クラス (子クラス)
public class Dog : Animal
{
// overrideキーワードを使用して、基底クラスのメソッドを再定義する
public override void MakeSound()
{
Console.WriteLine("ワンワン! (犬が鳴いています)");
}
}
class Program
{
static void Main(string[] args)
{
// Animal型の変数にDogのインスタンスを代入 (アップキャスト)
Animal myDog = new Dog();
// 実行されるのはDogクラスでオーバーライドされたメソッド
myDog.MakeSound();
// 通常のAnimalインスタンスの場合
Animal genericAnimal = new Animal();
genericAnimal.MakeSound();
}
}
}
ワンワン! (犬が鳴いています)
動物が音を出します。
このコードの重要なポイントは、変数myDogの型がAnimalであっても、実際に生成されたインスタンスがDogであれば、Dogクラスのメソッドが呼び出されるという点です。
これがポリモーフィズムの真髄であり、オーバーライドの最大のメリットです。
プロパティのオーバーライド
メソッドだけでなく、プロパティもオーバーライドの対象にできます。
プロパティに対してvirtualおよびoverrideを使用することで、取得 (get) や設定 (set) のロジックを派生クラスでカスタマイズできます。
public class BaseComponent
{
public virtual string Name { get; set; } = "Base";
}
public class CustomComponent : BaseComponent
{
private string _name = string.Empty;
public override string Name
{
get => $"Custom: {_name}";
set => _name = value;
}
}
abstract (抽象メソッド) との違い
オーバーライドを語る上で欠かせないのがabstractキーワードです。
virtualとabstractはどちらもoverrideによって上書きされますが、その性質は大きく異なります。
virtualメソッドの特徴
virtualメソッドは、「デフォルトの実装」を提供します。
派生クラスで必ずしもオーバーライドする必要はなく、上書きしなかった場合は親クラスの処理がそのまま実行されます。
これは「オプションのカスタマイズ」を提供したい場合に適しています。
abstractメソッドの特徴
一方でabstractメソッドは、「実装を持たず、定義のみ」を行います。
抽象クラス内で定義され、派生クラスに対して必ずオーバーライドすることを強制します。
共通の動作を強制しつつ、具体的な内容は各クラスに任せたい場合に適しています。
| 特徴 | virtual | abstract |
|---|---|---|
| 実装の有無 | 基底クラスで実装を持つ | 基底クラスでは実装を持てない |
| オーバーライドの義務 | 任意 (必要に応じて上書き) | 必須 (派生クラスで必ず実装) |
| クラスの制約 | 通常のクラスで使用可能 | 抽象クラス (abstract class) 内でのみ使用可能 |
abstractを使用したコード例
using System;
namespace AbstractExample
{
// 抽象クラス: インスタンス化はできない
public abstract class Shape
{
// 抽象メソッド: 派生クラスに実装を強制する
public abstract double GetArea();
public void Display()
{
Console.WriteLine($"この図形の面積は {GetArea()} です。");
}
}
public class Square : Shape
{
public double Side { get; set; }
// 抽象メソッドを必ず実装しなければならない
public override double GetArea()
{
return Side * Side;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
class Program
{
static void Main(string[] args)
{
Shape square = new Square { Side = 5 };
Shape circle = new Circle { Radius = 3 };
square.Display();
circle.Display();
}
}
}
この図形の面積は 25 です。
この図形の面積は 28.274333882308138 です。
overrideとnew (メソッド隠蔽) の決定的な違い
C#には、親クラスのメソッドと同じ名前のメソッドを子クラスで定義する方法として、オーバーライド以外にnew修飾子を用いたメソッド隠蔽 (Method Hiding)があります。
初心者の方が最も混同しやすいポイントですので、ここで明確に整理しましょう。
new修飾子による隠蔽とは
new修飾子を使用すると、基底クラスのメソッドとの関係を断ち切り、派生クラスで「全く別の同名メソッド」を定義することになります。
この場合、ポリモーフィズムは機能しません。
以下のコードでその違いを比較してみましょう。
using System;
public class BaseClass
{
public virtual void MethodA() => Console.WriteLine("Base MethodA (virtual)");
public void MethodB() => Console.WriteLine("Base MethodB (normal)");
}
public class DerivedClass : BaseClass
{
// 正当なオーバーライド
public override void MethodA() => Console.WriteLine("Derived MethodA (override)");
// メソッド隠蔽 (newキーワードを使用)
public new void MethodB() => Console.WriteLine("Derived MethodB (new)");
}
class Program
{
static void Main()
{
DerivedClass derived = new DerivedClass();
BaseClass baseRef = derived; // アップキャスト
Console.WriteLine("--- overrideの場合 ---");
derived.MethodA();
baseRef.MethodA(); // インスタンスの型(Derived)のメソッドが呼ばれる
Console.WriteLine("\n--- new (隠蔽) の場合 ---");
derived.MethodB();
baseRef.MethodB(); // 変数の型(Base)のメソッドが呼ばれる!
}
}
--- overrideの場合 ---
Derived MethodA (override)
Derived MethodA (override)
--- new (隠蔽) の場合 ---
Derived MethodB (new)
Base MethodB (normal)
この結果からわかるように、newで隠蔽されたメソッドは、参照している変数の型によって呼び出されるメソッドが変わってしまいます。
一方、overrideは変数の型に関わらず、常に「実際のインスタンス」に基づいたメソッドが呼び出されます。
意図せずに基底クラスと同じ名前のメソッドを定義してしまった場合、C#コンパイラは警告を出します。
その際、意図的に隠蔽したいのであればnewを、多態性を実現したいのであればvirtual/overrideを選択する必要があります。
基本的には、特別な理由がない限りoverrideを使用するのが安全です。
baseキーワードによる親クラスの呼び出し
オーバーライドを行う際、親クラスの実装を完全に捨てるのではなく、親クラスの処理を再利用しつつ独自の処理を追加したい場合があります。
このようなケースではbaseキーワードを使用します。
base.メソッド名()と記述することで、派生クラス内から基底クラスのメソッドを直接呼び出すことが可能です。
public class Logger
{
public virtual void Log(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
public class TimestampLogger : Logger
{
public override void Log(string message)
{
// 親クラスの処理を呼び出す
base.Log($"{DateTime.Now} - {message}");
}
}
この手法は、デザインパターンの「デコレーター」のような振る舞いを実現したり、フレームワークのライフサイクルメソッド (例えば Android開発の onCreate など) において親クラスの初期化処理を確実に実行させたりするために多用されます。
オーバーライドに関する詳細なルールと制約
オーバーライドを正しく実装するためには、いくつかの重要なルールを守る必要があります。
これらを無視するとコンパイルエラーが発生します。
1. アクセス修飾子の一致
基底クラスのvirtualメソッドと、派生クラスのoverrideメソッドは、アクセス修飾子が同じでなければなりません。
例えば、基底クラスでpublicとして定義されたメソッドを、派生クラスでprotectedとしてオーバーライドすることはできません。
2. シグネチャの一致
メソッド名、引数の数、引数の型、引数の順番がすべて一致している必要があります。
これらが異なると、オーバーライドではなく「オーバーロード (同名異メソッドの定義)」として扱われてしまいます。
3. staticメソッドはオーバーライド不可
static (静的) メソッドはクラス自体に属するものであり、インスタンスに基づいたポリモーフィズムの対象外です。
したがって、static virtualといった定義はできません。
4. 戻り値の型 (共変戻り値)
以前のC#では、戻り値の型も完全に一致させる必要がありました。
しかし、C# 9.0以降では共変戻り値 (Covariant Return Types)がサポートされ、基底クラスの戻り値型の派生クラスを戻り値として指定できるようになりました。
public class Food { }
public class Meat : Food { }
public class Animal
{
public virtual Food GetFood() => new Food();
}
public class Lion : Animal
{
// C# 9.0以降、Foodの派生クラスであるMeatを返せる
public override Meat GetFood() => new Meat();
}
5. sealed override による上書き禁止
派生クラスでオーバーライドしたメソッドを、さらにその先の継承先で上書きさせたくない場合は、sealed修飾子を組み合わせます。
public class FinalDog : Dog
{
// これ以降の派生クラスではMakeSoundをオーバーライドできない
public sealed override void MakeSound()
{
Console.WriteLine("これ以上上書きさせないワン!");
}
}
実践的な活用シーン:テンプレートメソッドパターン
オーバーライドの真価を発揮する設計手法の一つに、テンプレートメソッドパターン (Template Method Pattern)があります。
これは、アルゴリズムの全体的な流れを基底クラスで定義し、個別のステップの実装を派生クラスのオーバーライドに任せる手法です。
using System;
public abstract class DataProcessor
{
// アルゴリズムの骨組み (テンプレートメソッド)
public void Process()
{
ReadData();
TransformData();
SaveData();
}
protected abstract void ReadData();
protected virtual void TransformData()
{
Console.WriteLine("デフォルトの変換処理を適用します。");
}
protected abstract void SaveData();
}
public class CsvProcessor : DataProcessor
{
protected override void ReadData() => Console.WriteLine("CSVファイルを読み込みます。");
protected override void SaveData() => Console.WriteLine("CSVをデータベースに保存します。");
}
public class JsonProcessor : DataProcessor
{
protected override void ReadData() => Console.WriteLine("JSONファイルを読み込みます。");
protected override void TransformData() => Console.WriteLine("JSON固有の高度な変換を行います。");
protected override void SaveData() => Console.WriteLine("JSONをクラウドにアップロードします。");
}
このように、全体の「流れ」はProcessメソッドで固定しつつ、詳細な処理内容だけを各クラスでカスタマイズすることができます。
コードの重複を減らし、かつ柔軟な拡張を可能にする非常に強力なパターンです。
まとめ
本記事では、C#におけるオーバーライド (override)の仕組みと、関連するキーワードについて詳しく解説しました。
重要なポイントを振り返りましょう。
- オーバーライドは、基底クラスのメソッドを派生クラスで再定義し、ポリモーフィズムを実現するための仕組みです。
virtualはデフォルト実装を持つ「上書き可能なメソッド」、abstractは実装を持たず「上書きを強制するメソッド」です。newによるメソッド隠蔽は、参照型の変数によって動作が変わるため、通常はoverrideの使用が推奨されます。baseキーワードを使うことで、親クラスの機能を活かしながら機能を拡張できます。- C# 9.0以降の共変戻り値など、言語の進化によりさらに柔軟な記述が可能になっています。
オーバーライドを正しく使いこなすことで、変更に強く、直感的に理解しやすいクラス設計が可能になります。
まずは身近なプログラムの中で、共通化できそうな処理をvirtualやabstractを使って整理することから始めてみてください。
オブジェクト指向の真のパワーを実感できるはずです。
