C#のvirtualとoverrideとは?違い・使い方・newとの混乱をサンプルコードで完全解説

はじめに

C#で継承を利用していると、virtualoverridenewというキーワードが登場します。いずれも基底クラスと派生クラスの同名メソッドに関係しますが、動作は同じではありません。

virtualは、基底クラス側で「派生クラスによる処理の上書きを許可する」ための修飾子です。一方、overrideは、派生クラス側で「基底クラスの仮想メンバーを上書きする」ために使用します。

特に注意したいのが、overridenewの違いです。overrideでは実際に生成されたインスタンスの型によって呼び出される処理が決まりますが、newでは変数の宣言型によって呼び出し先が変わります。この違いを理解していないと、想定とは異なるメソッドが実行される原因になります。

この記事では、C#のvirtualoverrideの違い、基本的な使い方、newとの違い、関連キーワード、発生しやすいエラーをサンプルコード付きで解説します。

1. C#のvirtualとoverrideとは?最初に押さえたい基本

1-1. virtualは派生クラスでの上書きを許可する修飾子

virtualは、基底クラスのメソッドやプロパティなどに付ける修飾子です。対象のメンバーにvirtualを付けると、派生クラスでoverrideによる上書きが可能になります。

public class Animal{public virtual void Speak(){Console.WriteLine("動物が鳴きます");}}

この例では、AnimalクラスのSpeakメソッドが仮想メソッドとして定義されています。派生クラスは必要に応じて、Speakの処理を変更できます。

virtualメソッドには、通常のメソッドと同じように既定の処理を記述できます。そのため、派生クラスが上書きしなかった場合でも、そのまま実行可能です。

1-2. overrideは基底クラスの実装を上書きする修飾子

overrideは、基底クラスでvirtualまたはabstractなどが指定されたメンバーを、派生クラスで上書きするときに使用します。

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

DogクラスはAnimalクラスを継承し、Speakメソッドをオーバーライドしています。

Dog dog = new Dog();dog.Speak();

実行結果は次のとおりです。

ワン

基底クラスの「動物が鳴きます」ではなく、派生クラスで定義した「ワン」が出力されます。

1-3. virtualとoverrideは基底クラスと派生クラスでセットで使う

基本的な組み合わせは、次のようになります。

public class BaseClass{public virtual void Execute(){Console.WriteLine("基底クラスの処理");}}

public class DerivedClass : BaseClass{public override void Execute(){Console.WriteLine("派生クラスの処理");}}

基底クラス側ではvirtualを指定し、派生クラス側ではoverrideを指定します。

ただし、すべての派生クラスが必ずオーバーライドする必要はありません。基底クラスの処理をそのまま使用したい派生クラスでは、メソッドを定義しなくても問題ありません。

1-4. オーバーライドによって実現できるポリモーフィズムとは

ポリモーフィズムとは、同じ型や同じ操作を通じて、オブジェクトごとに異なる振る舞いを実現する仕組みです。日本語では「多態性」と呼ばれます。

public class Animal{public virtual void Speak(){Console.WriteLine("動物が鳴きます");}}

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

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

基底クラス型の変数に、それぞれ異なる派生クラスのインスタンスを代入できます。

Animal animal1 = new Dog();Animal animal2 = new Cat();

animal1.Speak();animal2.Speak();

実行結果は次のとおりです。

ワンニャー

変数の宣言型はどちらもAnimalですが、実際のインスタンスがDogならDog.SpeakCatならCat.Speakが呼び出されます。これがvirtualoverrideによるポリモーフィズムです。

2. C#のvirtualとoverrideの違い

2-1. virtualを記述する場所と役割

virtualは、基本的に基底クラス側のメンバーに記述します。

public class Notification{public virtual void Send(){Console.WriteLine("通知を送信します");}}

virtualの主な役割は、派生クラスに拡張ポイントを提供することです。

この例では、Notification.Sendに標準的な通知処理を用意しつつ、メール通知やプッシュ通知などの派生クラスが独自の処理に変更できるようにしています。

2-2. overrideを記述する場所と役割

overrideは、派生クラス側に記述します。

public class EmailNotification : Notification{public override void Send(){Console.WriteLine("メールを送信します");}}

overrideの役割は、継承した仮想メンバーの実装を派生クラスに適した処理へ置き換えることです。

オーバーライドするには、基底クラスのメンバーが次のいずれかとして定義されている必要があります。

  • virtual

  • abstract

  • override

すでにオーバーライドされたメソッドも、既定ではさらに下位の派生クラスでオーバーライドできます。

2-3. virtualだけ指定した場合の動作

基底クラスのメソッドにvirtualを付けても、派生クラスが必ずオーバーライドするわけではありません。

public class UserService{public virtual void Save(){Console.WriteLine("ユーザー情報を保存します");}}

public class AdminUserService : UserService{}

AdminUserServiceではSaveをオーバーライドしていないため、基底クラスの処理が使用されます。

AdminUserService service = new AdminUserService();service.Save();

実行結果は次のとおりです。

ユーザー情報を保存します

virtualは上書きを強制するものではなく、上書きを許可するものです。

2-4. overrideしない派生クラスでは基底クラスの実装が使われる

複数の派生クラスがある場合、一部のクラスだけがメソッドをオーバーライドする設計も可能です。

public class Report{public virtual void Export(){Console.WriteLine("標準形式で出力します");}}

public class PdfReport : Report{public override void Export(){Console.WriteLine("PDF形式で出力します");}}

public class TextReport : Report{}

次のコードを実行します。

Report pdf = new PdfReport();Report text = new TextReport();

pdf.Export();text.Export();

実行結果は次のとおりです。

PDF形式で出力します標準形式で出力します

PdfReportでは独自の処理が呼び出され、TextReportでは基底クラスの処理がそのまま使われます。

2-5. virtualとoverrideの違いを比較表で確認

比較項目virtualoverride
主に記述する場所基底クラス派生クラス
役割派生クラスでの上書きを許可する基底クラスの仮想メンバーを上書きする
既定の実装記述できる派生クラス独自の実装を記述する
単独で利用できるかオーバーライドされなくても利用できる上書き可能な基底メンバーが必要
呼び出し先の決定オーバーライドされた場合は実行時に決まる実際のインスタンス型に対応する実装が呼ばれる
対象メソッド、プロパティ、インデクサー、イベント上書き可能な同種のメンバー
staticへの指定できないできない

3. virtualとoverrideの基本的な使い方

3-1. 基底クラスにvirtualメソッドを定義する

最初に、共通の処理を持つ基底クラスを作成します。

public class Payment{public virtual void Pay(decimal amount){Console.WriteLine($"{amount:N0}円を支払います");}}

Payメソッドには標準的な支払い処理を記述し、派生クラスで変更できるようにvirtualを付けています。

3-2. 派生クラスでメソッドをoverrideする

次に、基底クラスを継承して処理を上書きします。

public class CreditCardPayment : Payment{public override void Pay(decimal amount){Console.WriteLine($"クレジットカードで{amount:N0}円を支払います");}}

オーバーライドするメソッドは、基底クラスのメソッドと名前、引数、アクセスレベルなどを対応させる必要があります。

3-3. インスタンスを生成してオーバーライド後の処理を呼び出す

派生クラスのインスタンスからPayを呼び出します。

CreditCardPayment payment = new CreditCardPayment();payment.Pay(5000);

実行結果は次のとおりです。

クレジットカードで5,000円を支払います

派生クラスで上書きされた処理が実行されます。

3-4. 基底クラス型の変数から派生クラスのメソッドが呼ばれる仕組み

overrideの重要な特徴は、基底クラス型の変数から呼び出した場合でも、派生クラスの実装が選択されることです。

Payment payment = new CreditCardPayment();payment.Pay(5000);

変数paymentの宣言型はPaymentですが、格納されている実際のインスタンスはCreditCardPaymentです。そのため、実行結果は次のようになります。

クレジットカードで5,000円を支払います

仮想メンバーの呼び出し先は、実行時のインスタンス型に基づいて決定されます。この仕組みは動的ディスパッチとも呼ばれます。

3-5. 複数の派生クラスで処理を切り替えるサンプルコード

public class Payment{public virtual void Pay(decimal amount){Console.WriteLine($"{amount:N0}円を支払います");}}

public class CreditCardPayment : Payment{public override void Pay(decimal amount){Console.WriteLine($"クレジットカードで{amount:N0}円を支払います");}}

public class CashPayment : Payment{public override void Pay(decimal amount){Console.WriteLine($"現金で{amount:N0}円を支払います");}}

public class PointPayment : Payment{public override void Pay(decimal amount){Console.WriteLine($"{amount:N0}ポイントを使用します");}}

基底クラス型の配列にまとめて格納できます。

Payment[] payments ={new CreditCardPayment(),new CashPayment(),new PointPayment()};

foreach (Payment payment in payments){payment.Pay(3000);}

実行結果は次のとおりです。

クレジットカードで3,000円を支払います現金で3,000円を支払います3,000ポイントを使用します

呼び出す側は個別の派生クラスを判定する必要がありません。各クラスに処理の違いを任せられるため、条件分岐を減らしやすくなります。

4. overrideとnewの違い|混乱しやすいメソッドの上書きと隠蔽

4-1. newは基底クラスのメソッドを上書きせず隠蔽する

C#のnew修飾子は、基底クラスの同名メンバーを隠蔽するときに使用します。

public class BaseClass{public void Show(){Console.WriteLine("基底クラス");}}

public class DerivedClass : BaseClass{public new void Show(){Console.WriteLine("派生クラス");}}

このコードでは、DerivedClass.ShowBaseClass.Showをオーバーライドしているわけではありません。基底クラスの同名メソッドとは別のメソッドを定義し、派生クラスから見えにくくしているだけです。

4-2. overrideは実行時のインスタンス型によって呼び出し先が決まる

overrideでは、変数の宣言型ではなく、実際に格納されているインスタンスの型によって呼び出し先が決まります。

public class BaseClass{public virtual void Show(){Console.WriteLine("基底クラス");}}

public class DerivedClass : BaseClass{public override void Show(){Console.WriteLine("派生クラス");}}

BaseClass value = new DerivedClass();value.Show();

実行結果は次のとおりです。

派生クラス

変数はBaseClass型ですが、実体がDerivedClassであるため、オーバーライドされたメソッドが呼ばれます。

4-3. newは変数の宣言型によって呼び出し先が決まる

newでメソッドを隠蔽した場合、呼び出し先は変数の宣言型によって決まります。

public class BaseClass{public void Show(){Console.WriteLine("基底クラス");}}

public class DerivedClass : BaseClass{public new void Show(){Console.WriteLine("派生クラス");}}

DerivedClass derived = new DerivedClass();BaseClass baseValue = derived;

derived.Show();baseValue.Show();

実行結果は次のとおりです。

派生クラス基底クラス

どちらの変数も同じインスタンスを参照していますが、宣言型が異なるため、呼び出されるメソッドも異なります。

4-4. overrideとnewの動作を同じサンプルコードで比較

overridenewの違いを、同じ構成のコードで比較してみましょう。

public class Parent{public virtual void VirtualMethod(){Console.WriteLine("Parent.VirtualMethod");}

public void NormalMethod(){Console.WriteLine("Parent.NormalMethod");}

}

public class Child : Parent{public override void VirtualMethod(){Console.WriteLine("Child.VirtualMethod");}

public new void NormalMethod(){Console.WriteLine("Child.NormalMethod");}

}

次のように呼び出します。

Child child = new Child();Parent parent = child;

child.VirtualMethod();parent.VirtualMethod();

child.NormalMethod();parent.NormalMethod();

実行結果は次のとおりです。

Child.VirtualMethodChild.VirtualMethodChild.NormalMethodParent.NormalMethod

VirtualMethodoverrideされているため、変数の宣言型に関係なくChildの処理が実行されます。

一方、NormalMethodnewで隠蔽されているため、Child型の変数ではChild.NormalMethodParent型の変数ではParent.NormalMethodが呼ばれます。

4-5. newを省略して同名メソッドを定義したときに警告が出る理由

基底クラスと同名・同シグネチャのメソッドを派生クラスに定義すると、newを省略していてもメンバーの隠蔽は発生します。

public class Parent{public void Show(){Console.WriteLine("Parent");}}

public class Child : Parent{public void Show(){Console.WriteLine("Child");}}

この場合、コンパイラは一般に「継承されたメンバーを隠しています。意図した場合はnewキーワードを使用してください」という趣旨の警告を出します。

警告が出る理由は、開発者が次のどちらを意図しているのか、コードから明確に判断できないためです。

  • 意図的に基底クラスのメソッドを隠したい

  • 本当はオーバーライドしたいが、virtualoverrideを付け忘れた

隠蔽を意図している場合は、newを明示します。

public new void Show(){Console.WriteLine("Child");}

ただし、警告を消すためだけにnewを付けるのではなく、隠蔽が適切な設計かを確認することが重要です。

4-6. overrideとnewをどちらにするべきか判断する基準

基底クラスと派生クラスを同じ抽象概念として扱い、実際のインスタンスに応じて振る舞いを切り替えたい場合はoverrideを使用します。

たとえば、すべての動物をAnimalとして扱いながら、犬と猫で鳴き声を変更したい場合はoverrideが適しています。

Animal animal = new Dog();animal.Speak();

一方、newは次のような限定的な場面で使用されます。

  • 基底クラスのメンバーがvirtualではなく、変更もできない

  • 同名メンバーを別の意味で公開する必要がある

  • 基底クラスのAPI変更に対応する必要がある

  • 隠蔽による型ごとの動作差を理解したうえで意図的に利用する

通常のポリモーフィズムを実現したい場合は、newではなくvirtualoverrideを選択するのが基本です。

5. virtual・overrideと関連キーワードの違い

5-1. virtualとabstractの違い|既定の実装があるか

virtualabstractは、どちらも派生クラスでオーバーライドできるメンバーを定義するために使用します。主な違いは、基底クラス側に既定の実装があるかどうかです。

virtualメソッドには処理を記述できます。

public class Logger{public virtual void Write(string message){Console.WriteLine(message);}}

派生クラスは、必要な場合だけオーバーライドします。

一方、abstractメソッドは処理本体を持ちません。

public abstract class Shape{public abstract double GetArea();}

非抽象の派生クラスは、抽象メソッドを実装する必要があります。

public class Circle : Shape{public double Radius { get; }

public Circle(double radius){Radius = radius;}public override double GetArea(){return Math.PI * Radius * Radius;}

}

使い分けの基準は次のとおりです。

  • 共通の既定処理があり、必要なクラスだけ変更するならvirtual

  • 基底クラスでは処理を決められず、派生クラスに実装を強制するならabstract

また、abstractメンバーはabstractクラス内でのみ宣言できます。

5-2. overrideとオーバーロードの違い

オーバーライドとオーバーロードは名前が似ていますが、異なる仕組みです。

オーバーライドは、継承関係にあるクラスで基底クラスのメソッドを上書きすることです。

public class Parent{public virtual void Execute(){}}

public class Child : Parent{public override void Execute(){}}

オーバーロードは、同じ名前で引数の数や型が異なる複数のメソッドを定義することです。

public class Calculator{public int Add(int left, int right){return left + right;}

public double Add(double left, double right){return left + right;}public int Add(int first, int second, int third){return first + second + third;}

}

主な違いは次のとおりです。

比較項目オーバーライドオーバーロード
継承必要不要
メソッド名同じ同じ
引数基本的に同じ数または型を変える
目的派生クラスで処理を変更する同じ操作を複数の入力形式に対応させる
呼び出し先実行時の型が関係するコンパイル時に引数から選ばれる

5-3. baseを使って基底クラスの処理を呼び出す方法

派生クラスで処理を完全に置き換えるのではなく、基底クラスの処理を実行したうえで機能を追加したい場合は、baseを使用します。

public class Document{public virtual void Save(){Console.WriteLine("ドキュメントを保存します");}}

public class EncryptedDocument : Document{public override void Save(){Console.WriteLine("データを暗号化します");base.Save();Console.WriteLine("暗号化した状態で保存しました");}}

実行例は次のとおりです。

Document document = new EncryptedDocument();document.Save();

実行結果は次のとおりです。

データを暗号化しますドキュメントを保存します暗号化した状態で保存しました

base.Save()を記述した位置で、直接の基底クラスに定義されているSaveの実装が呼び出されます。

基底処理の前に追加処理を行うか、後に行うかは設計によって決めます。

5-4. sealed overrideでそれ以上のオーバーライドを禁止する方法

通常、overrideしたメソッドは、さらに下位の派生クラスで再度オーバーライドできます。

public class BaseClass{public virtual void Execute(){}}

public class MiddleClass : BaseClass{public override void Execute(){}}

public class FinalClass : MiddleClass{public override void Execute(){}}

途中のクラスで、それ以上のオーバーライドを禁止したい場合はsealed overrideを使用します。

public class BaseClass{public virtual void Execute(){Console.WriteLine("BaseClass");}}

public class MiddleClass : BaseClass{public sealed override void Execute(){Console.WriteLine("MiddleClass");}}

次のコードはコンパイルエラーになります。

public class FinalClass : MiddleClass{public override void Execute(){Console.WriteLine("FinalClass");}}

sealed overrideは、クラス自体の継承は許可しながら、特定のメンバーだけ変更を禁止したい場合に利用できます。

5-5. interfaceによる実装とvirtual・overrideによる継承の違い

インターフェースは「何ができるか」という契約を表し、クラス継承は共通の状態や処理を受け継ぐ仕組みです。

public interface IPrintable{void Print();}

インターフェースを実装するクラスは、必要なメンバーを用意します。

public class Invoice : IPrintable{public void Print(){Console.WriteLine("請求書を印刷します");}}

一方、virtualoverrideでは、基底クラスに共通処理を持たせられます。

public class Document{public virtual void Print(){Console.WriteLine("文書を印刷します");}}

public class Invoice : Document{public override void Print(){Console.WriteLine("請求書を印刷します");}}

主な違いは次のとおりです。

比較項目インターフェースvirtual・override
主な目的振る舞いの契約を定義する基底クラスの振る舞いを拡張・変更する
複数指定複数のインターフェースを実装できるクラス継承は1つだけ
状態の共有通常はインスタンスフィールドを持たない基底クラスのフィールドや状態を共有できる
共通処理既定のインターフェース実装は可能だが用途が異なる基底クラスに自然に共通処理を持たせられる
関係性「できること」を表しやすい「~の一種」という関係を表しやすい

異なる種類のクラスに共通の機能を求める場合はインターフェース、密接な継承関係と共通実装が必要な場合は基底クラスを検討します。

5-6. staticメソッドをoverrideできない理由

staticメソッドは、インスタンスではなくクラスそのものに属します。

public class Utility{public static void Execute(){Console.WriteLine("実行");}}

呼び出すときも、インスタンスではなく型名を使用します。

Utility.Execute();

overrideによるポリモーフィズムは、実際のインスタンス型をもとに呼び出し先を決める仕組みです。staticメソッドには呼び出し対象となるインスタンスがないため、virtualoverrideは指定できません。

派生クラスに同名のstaticメソッドを定義することはできますが、それはオーバーライドではなく隠蔽です。

public class Parent{public static void Show(){Console.WriteLine("Parent");}}

public class Child : Parent{public new static void Show(){Console.WriteLine("Child");}}

呼び出し先は型名によって明示的に決まります。

Parent.Show();Child.Show();

6. メソッド以外でvirtualとoverrideを使う方法

6-1. プロパティをvirtual・overrideするサンプルコード

virtualoverrideはメソッドだけでなく、プロパティにも使用できます。

public class Product{public virtual decimal Price { get; set; }}

public class DiscountProduct : Product{public override decimal Price{get => base.Price * 0.9m;set => base.Price = value;}}

利用例は次のとおりです。

Product product = new DiscountProduct{Price = 1000m};

Console.WriteLine(product.Price);

実行結果は次のとおりです。

900.0

変数の宣言型はProductですが、実際のインスタンスがDiscountProductなので、オーバーライドされたプロパティが使用されます。

実務では、値の保存先となるフィールドを基底クラスに用意すると、意図がより明確になります。

public class Product{protected decimal basePrice;

public virtual decimal Price{get => basePrice;set => basePrice = value;}

}

6-2. インデクサーをvirtual・overrideする場合

インデクサーにもvirtualoverrideを指定できます。

public class MessageCollection{protected readonly string[] messages ={"Hello","Good morning","Good night"};

public virtual string this<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[int index]</span>{get =&gt; messages<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[index]</span>;}

}

public class JapaneseMessageCollection : MessageCollection{public override string this[int index]{get{return index switch{0 => "こんにちは",1 => "おはようございます",2 => "おやすみなさい",_ => throw new IndexOutOfRangeException()};}}}

利用例は次のとおりです。

MessageCollection messages =new JapaneseMessageCollection();

Console.WriteLine(messages[0]);

実行結果は次のとおりです。

こんにちは

通常のプロパティと同様に、実際のインスタンス型に対応するインデクサーが呼び出されます。

6-3. イベントをvirtual・overrideする場合

イベントにもvirtualoverrideを使用できます。

public class Worker{public virtual event EventHandler? Completed;

public virtual void Run(){Console.WriteLine("処理を実行します");OnCompleted();}protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}

}

イベント自体をオーバーライドすることも可能です。

public class LoggingWorker : Worker{private EventHandler? completed;

public override event EventHandler? Completed{add{Console.WriteLine("イベントを登録しました");completed += value;}remove{Console.WriteLine("イベントを解除しました");completed -= value;}}protected override void OnCompleted(){Console.WriteLine("完了イベントを通知します");completed?.Invoke(this, EventArgs.Empty);}

}

ただし、イベントのオーバーライドは実装が複雑になりやすいため、一般的にはイベントを発生させるOnCompletedのようなprotected virtualメソッドを用意し、そのメソッドをオーバーライドする設計がよく使われます。

6-4. getterとsetterのアクセス範囲で注意するポイント

プロパティをオーバーライドするときは、基底プロパティに存在するアクセサーとの対応に注意が必要です。

public class BaseClass{public virtual string Name { get; protected set; } = "";}

派生クラスでは、次のようにオーバーライドできます。

public class DerivedClass : BaseClass{public override string Name{get => base.Name;protected set => base.Name = value;}}

オーバーライド時に、メンバー全体のアクセス修飾子を自由に変更することはできません。また、基底プロパティに存在しないアクセサーを勝手に追加することもできません。

アクセサーごとのアクセス範囲は、基底クラスの契約を維持するように設計する必要があります。

7. virtualとoverrideで発生しやすいエラーと解決方法

7-1. virtualがないメソッドをoverrideしてコンパイルエラーになる

基底クラスの通常メソッドを、派生クラスでoverrideすることはできません。

public class Parent{public void Execute(){}}

public class Child : Parent{public override void Execute(){}}

Parent.Executevirtualabstract、またはoverrideが指定されていないため、コンパイルエラーになります。

オーバーライドしたい場合は、基底クラス側を次のように変更します。

public class Parent{public virtual void Execute(){}}

基底クラスを変更できない場合、overrideは使用できません。意図的な隠蔽が必要ならnewを検討しますが、ポリモーフィズムの動作は得られません。

7-2. メソッド名や引数の型が一致せずoverrideできない

オーバーライドするメソッドは、基底クラスのメソッドと対応するシグネチャを持つ必要があります。

public class Parent{public virtual void Print(string message){}}

次のコードでは、引数の型が異なります。

public class Child : Parent{public override void Print(int message){}}

これはPrint(string)のオーバーライドにはならず、コンパイルエラーになります。

正しくは次のように記述します。

public class Child : Parent{public override void Print(string message){Console.WriteLine($"Child: {message}");}}

引数の型だけでなく、引数の数や型引数なども対応させる必要があります。

7-3. 戻り値の型が合わずエラーになる

基本的に、オーバーライドするメソッドの戻り値は基底メソッドと対応している必要があります。

public class Parent{public virtual int GetValue(){return 1;}}

次のように戻り値をstringへ変更することはできません。

public class Child : Parent{public override string GetValue(){return "1";}}

正しくは、同じintを使用します。

public class Child : Parent{public override int GetValue(){return 2;}}

新しいC#では、参照型を返すメソッドについて共変戻り値が利用できる場合があります。たとえば、基底メソッドがAnimalを返し、派生メソッドがその派生型であるDogを返す形です。

public class Animal{}

public class Dog : Animal{}

public class AnimalFactory{public virtual Animal Create(){return new Animal();}}

public class DogFactory : AnimalFactory{public override Dog Create(){return new Dog();}}

ただし、無関係な型へ自由に変更できるわけではありません。

7-4. アクセス修飾子を変更してエラーになる

オーバーライド時にアクセス修飾子を変更することはできません。

public class Parent{public virtual void Execute(){}}

次のコードはエラーです。

public class Child : Parent{protected override void Execute(){}}

基底クラスでpublicなら、派生クラスでもpublicにします。

public class Child : Parent{public override void Execute(){}}

オーバーライドによって公開範囲を狭めたり広げたりすると、基底クラスが提供している契約が変わってしまうためです。

7-5. privateメソッドをoverrideできない

privateメソッドは、そのメソッドを宣言したクラスの内部からしかアクセスできません。派生クラスから見えないため、オーバーライドもできません。

public class Parent{private void Execute(){}}

派生クラスから変更可能にしたい場合は、たとえばprotected virtualにします。

public class Parent{protected virtual void Execute(){Console.WriteLine("Parent");}}

public class Child : Parent{protected override void Execute(){Console.WriteLine("Child");}}

外部には公開せず、派生クラスだけに拡張を許可したい場合に適した形です。

7-6. sealedクラスやsealed overrideを継承してエラーになる

sealedクラスは継承できません。

public sealed class FinalService{}

次のコードはコンパイルエラーになります。

public class CustomService : FinalService{}

また、sealed overrideが指定されたメソッドは、それ以上オーバーライドできません。

public class Parent{public virtual void Execute(){}}

public class Child : Parent{public sealed override void Execute(){}}

次のクラスで再度オーバーライドしようとするとエラーになります。

public class GrandChild : Child{public override void Execute(){}}

エラーを解決するには、設計上変更可能ならsealedを外すか、別の拡張方法を検討します。

7-7. newとoverrideを取り違えて想定外のメソッドが呼ばれる

次のコードでは、newによってメソッドが隠蔽されています。

public class Parent{public virtual void Show(){Console.WriteLine("Parent");}}

public class Child : Parent{public new void Show(){Console.WriteLine("Child");}}

呼び出し方によって結果が変わります。

Child child = new Child();Parent parent = child;

child.Show();parent.Show();

実行結果は次のとおりです。

ChildParent

基底クラスのメソッドがvirtualであっても、派生クラス側でnewを付ければオーバーライドにはなりません。

ポリモーフィズムを利用したいなら、次のようにoverrideへ変更します。

public class Child : Parent{public override void Show(){Console.WriteLine("Child");}}

8. virtualとoverrideを使うべき場面

8-1. 基底クラスに共通処理を持たせながら一部だけ変更したい場合

複数のクラスで大部分の処理が共通し、一部だけ異なる場合はvirtualoverrideが役立ちます。

public class FileImporter{public void Import(string path){Validate(path);Read(path);Complete();}

protected virtual void Validate(string path){Console.WriteLine("共通の入力チェック");}protected virtual void Read(string path){Console.WriteLine($"ファイルを読み込みます: {path}");}protected virtual void Complete(){Console.WriteLine("インポート完了");}

}

CSVだけ読み込み処理を変更する場合は、必要な箇所だけオーバーライドできます。

public class CsvImporter : FileImporter{protected override void Read(string path){Console.WriteLine($"CSVを解析します: {path}");}}

共通処理を重複して実装せずに済む点がメリットです。

8-2. 派生クラスごとに異なる振る舞いを実装したい場合

同じ操作でもクラスごとに処理が異なる場合は、オーバーライドによるポリモーフィズムが適しています。

public abstract class Delivery{public abstract decimal CalculateFee();}

public class StandardDelivery : Delivery{public override decimal CalculateFee(){return 500m;}}

public class ExpressDelivery : Delivery{public override decimal CalculateFee(){return 1000m;}}

呼び出し側は個々の配送方法を判定せず、共通のCalculateFeeを呼び出せます。

decimal GetFee(Delivery delivery){return delivery.CalculateFee();}

派生クラスが増えても、呼び出し側の条件分岐を増やさずに対応しやすくなります。

8-3. テンプレートメソッドパターンで処理の一部を差し替える場合

テンプレートメソッドパターンは、処理全体の流れを基底クラスで定義し、一部の工程だけを派生クラスで変更する設計パターンです。

public abstract class DataProcessor{public void Process(){Load();Transform();Save();}

protected virtual void Load(){Console.WriteLine("データを読み込みます");}protected abstract void Transform();protected virtual void Save(){Console.WriteLine("データを保存します");}

}

派生クラスでは、必要な工程だけを実装または変更します。

public class CustomerDataProcessor : DataProcessor{protected override void Transform(){Console.WriteLine("顧客データを変換します");}

protected override void Save(){Console.WriteLine("顧客データベースへ保存します");}

}

処理順序を基底クラスに固定しつつ、派生クラスごとの差分を明確にできます。

8-4. Unityの継承設計でvirtualとoverrideを活用する場合

Unityのゲーム開発でも、独自の基底クラスを作成してキャラクターごとの振る舞いを変更する場合にvirtualoverrideを利用できます。

using UnityEngine;

public class Character : MonoBehaviour{public virtual void Attack(){Debug.Log("通常攻撃");}}

public class Warrior : Character{public override void Attack(){Debug.Log("剣で攻撃");}}

public class Mage : Character{public override void Attack(){Debug.Log("魔法で攻撃");}}

呼び出し側はCharacter型として扱えます。

public class BattleController : MonoBehaviour{[SerializeField]private Character character;

public void ExecuteAttack(){character.Attack();}

}

インスペクターでWarriorまたはMageを割り当てれば、それぞれのAttackが実行されます。

ただし、UnityのStartUpdateOnTriggerEnterなどのメッセージメソッドが、すべて自動的に仮想メソッドとして定義されているわけではありません。独自の継承階層で確実にオーバーライドしたい場合は、基底クラス側で明示的にvirtualまたはprotected virtualとして設計します。

public class BaseCharacter : MonoBehaviour{protected virtual void Start(){Debug.Log("共通の初期化");}}

public class PlayerCharacter : BaseCharacter{protected override void Start(){base.Start();Debug.Log("プレイヤー固有の初期化");}}

8-5. virtualを安易に付けないほうがよい理由

virtualを付けると拡張しやすくなりますが、すべてのメソッドを仮想化すればよいわけではありません。

仮想メソッドは、派生クラスによって処理内容が変更される可能性があります。そのため、基底クラス側で想定していた前提条件や不変条件が崩れることがあります。

たとえば、必ず入力検証を実行すべきメソッド全体をvirtualにすると、派生クラスが検証を省略できてしまいます。

public class AccountService{public virtual void Withdraw(decimal amount){ValidateAmount(amount);Console.WriteLine($"{amount:N0}円を出金します");}

private void ValidateAmount(decimal amount){if (amount &lt;= 0){throw new ArgumentOutOfRangeException(nameof(amount));}}

}

この場合、処理全体をオーバーライド可能にするより、差し替えたい部分だけを保護された仮想メソッドにする設計が考えられます。

public class AccountService{public void Withdraw(decimal amount){ValidateAmount(amount);ExecuteWithdrawal(amount);}

private void ValidateAmount(decimal amount){if (amount &lt;= 0){throw new ArgumentOutOfRangeException(nameof(amount));}}protected virtual void ExecuteWithdrawal(decimal amount){Console.WriteLine($"{amount:N0}円を出金します");}

}

外部に保証すべき処理の流れは固定し、安全に変更できる箇所だけをvirtualにすることが重要です。

8-6. 継承よりコンポジションを選ぶべきケース

継承は強い結合を生みます。派生クラスは基底クラスの設計や変更の影響を受けるため、単にコードを再利用したいだけならコンポジションのほうが適している場合があります。

たとえば、通知方法を切り替えるためだけに継承を使うと、クラス階層が複雑になる可能性があります。

コンポジションでは、必要な機能を別オブジェクトとして保持します。

public interface IMessageSender{void Send(string message);}

public class EmailSender : IMessageSender{public void Send(string message){Console.WriteLine($"メール送信: {message}");}}

public class SmsSender : IMessageSender{public void Send(string message){Console.WriteLine($"SMS送信: {message}");}}

利用するクラスは、送信機能を受け取ります。

public class NotificationService{private readonly IMessageSender sender;

public NotificationService(IMessageSender sender){this.sender = sender;}public void Notify(string message){sender.Send(message);}

}

利用例は次のとおりです。

var emailService =new NotificationService(new EmailSender());

var smsService =new NotificationService(new SmsSender());

emailService.Notify("処理が完了しました");smsService.Notify("処理が完了しました");

次のような場合は、継承よりコンポジションを検討します。

  • 単に機能を再利用したい

  • 実行中に振る舞いを切り替えたい

  • 複数の独立した機能を組み合わせたい

  • 「派生クラスは基底クラスの一種である」と自然にいえない

  • 継承階層が深くなり、変更の影響を把握しにくい

9. virtualとoverrideの実践サンプルコード

9-1. 図形ごとに面積計算を切り替えるサンプル

図形を表す基底クラスを作成し、派生クラスごとに面積計算を変更します。

public abstract class Shape{public abstract double GetArea();

public virtual void PrintArea(){Console.WriteLine($"面積: {GetArea():F2}");}

}

public class Circle : Shape{public double Radius { get; }

public Circle(double radius){Radius = radius;}public override double GetArea(){return Math.PI * Radius * Radius;}

}

public class Rectangle : Shape{public double Width { get; }public double Height { get; }

public Rectangle(double width, double height){Width = width;Height = height;}public override double GetArea(){return Width * Height;}

}

実行例は次のとおりです。

Shape circle = new Circle(3);Shape rectangle = new Rectangle(4, 5);

circle.PrintArea();rectangle.PrintArea();

実行結果は次のようになります。

面積: 28.27面積: 20.00

PrintAreaは基底クラスの共通処理ですが、その内部で呼び出されるGetAreaは実際の図形に応じて切り替わります。

9-2. キャラクターごとに攻撃処理を変更するサンプル

public class Character{public string Name { get; }

public Character(string name){Name = name;}public virtual void Attack(){Console.WriteLine($"{Name}は攻撃した");}

}

public class Warrior : Character{public Warrior(string name) : base(name){}

public override void Attack(){Console.WriteLine($"{Name}は剣で攻撃した");}

}

public class Mage : Character{public Mage(string name) : base(name){}

public override void Attack(){Console.WriteLine($"{Name}は炎の魔法を使った");}

}

public class Healer : Character{public Healer(string name) : base(name){}

public override void Attack(){Console.WriteLine($"{Name}は杖で攻撃した");}

}

利用例は次のとおりです。

Character[] characters ={new Warrior("アレン"),new Mage("リナ"),new Healer("ミア")};

foreach (Character character in characters){character.Attack();}

実行結果は次のとおりです。

アレンは剣で攻撃したリナは炎の魔法を使ったミアは杖で攻撃した

9-3. baseを使って共通処理を残しながら機能を追加するサンプル

public class OrderService{public virtual void CompleteOrder(int orderId){Console.WriteLine($"注文{orderId}を完了状態に変更しました");}}

public class NotifyingOrderService : OrderService{public override void CompleteOrder(int orderId){base.CompleteOrder(orderId);

    Console.WriteLine($"注文{orderId}の完了メールを送信しました");}

}

実行例は次のとおりです。

OrderService service =new NotifyingOrderService();

service.CompleteOrder(1001);

実行結果は次のとおりです。

注文1001を完了状態に変更しました注文1001の完了メールを送信しました

base.CompleteOrderによって共通の注文完了処理を残し、その後に通知機能を追加しています。

9-4. Listに異なる派生クラスを格納して一括処理するサンプル

public abstract class Employee{public string Name { get; }

protected Employee(string name){Name = name;}public abstract decimal CalculateSalary();

}

public class FullTimeEmployee : Employee{public decimal MonthlySalary { get; }

public FullTimeEmployee(string name,decimal monthlySalary) : base(name){MonthlySalary = monthlySalary;}public override decimal CalculateSalary(){return MonthlySalary;}

}

public class PartTimeEmployee : Employee{public decimal HourlyRate { get; }public int WorkedHours { get; }

public PartTimeEmployee(string name,decimal hourlyRate,int workedHours) : base(name){HourlyRate = hourlyRate;WorkedHours = workedHours;}public override decimal CalculateSalary(){return HourlyRate * WorkedHours;}

}

異なる派生クラスをList<Employee>へ格納します。

var employees = new List<Employee>{new FullTimeEmployee("田中", 320000m),new PartTimeEmployee("佐藤", 1500m, 80),new FullTimeEmployee("鈴木", 400000m)};

foreach (Employee employee in employees){Console.WriteLine($"{employee.Name}: " +$"{employee.CalculateSalary():N0}円");}

実行結果は次のとおりです。

田中: 320,000円佐藤: 120,000円鈴木: 400,000円

呼び出し側は正社員かパート社員かを条件分岐で判定していません。それぞれのクラスが給与計算の責任を持ちます。

9-5. overrideとnewで実行結果が変わることを確認するサンプル

次のコードは、overridenewを同時に比較できます。

public class Device{public virtual void Start(){Console.WriteLine("Device.Start");}

public void Stop(){Console.WriteLine("Device.Stop");}

}

public class Computer : Device{public override void Start(){Console.WriteLine("Computer.Start");}

public new void Stop(){Console.WriteLine("Computer.Stop");}

}

次のコードを実行します。

Computer computer = new Computer();Device device = computer;

Console.WriteLine("Computer型から呼び出し");computer.Start();computer.Stop();

Console.WriteLine("Device型から呼び出し");device.Start();device.Stop();

実行結果は次のとおりです。

Computer型から呼び出しComputer.StartComputer.StopDevice型から呼び出しComputer.StartDevice.Stop

Startはオーバーライドされているため、どちらの変数から呼び出してもComputer.Startが実行されます。

Stopnewで隠蔽されているため、Computer型からはComputer.StopDevice型からはDevice.Stopが実行されます。

10. C#のvirtualとoverrideに関するよくある質問

10-1. virtualを付けないメソッドは上書きできない?

virtualabstract、またはoverrideが指定されていないメソッドを、派生クラスでオーバーライドすることはできません。

同名メソッドを派生クラスに定義することはできますが、それは上書きではなく隠蔽です。

public class Parent{public void Show(){}}

public class Child : Parent{public new void Show(){}}

実行時のインスタンス型に応じて処理を切り替えたい場合は、基底メソッドにvirtualを付け、派生メソッドにoverrideを付けます。

10-2. overrideをさらに派生クラスでoverrideできる?

できます。オーバーライドされたメソッドは、既定ではさらに下位の派生クラスでオーバーライド可能です。

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

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

public class GuideDog : Dog{public override void Speak(){Console.WriteLine("GuideDog");}}

それ以上のオーバーライドを禁止したい場合は、途中のクラスでsealed overrideを指定します。

public sealed override void Speak(){Console.WriteLine("Dog");}

10-3. overrideしたメソッドから元の処理も実行できる?

baseを使用すれば、基底クラスの処理を呼び出せます。

public class Parent{public virtual void Execute(){Console.WriteLine("共通処理");}}

public class Child : Parent{public override void Execute(){base.Execute();Console.WriteLine("追加処理");}}

実行結果は次のとおりです。

共通処理追加処理

base.Execute()を呼ばなければ、基底クラスの実装は自動では実行されません。

10-4. コンストラクターにvirtualやoverrideは付けられる?

コンストラクターにvirtualoverrideを付けることはできません。

コンストラクターは継承される通常メンバーではなく、各クラスが自身のインスタンスを初期化するための特別なメンバーだからです。

派生クラスのコンストラクターから基底クラスのコンストラクターを呼び出す場合は、baseを使用します。

public class Parent{public Parent(string name){Console.WriteLine($"Parent: {name}");}}

public class Child : Parent{public Child(string name) : base(name){Console.WriteLine($"Child: {name}");}}

また、コンストラクターからvirtualメソッドを呼び出す設計には注意が必要です。派生クラスの初期化が完了する前にオーバーライドされたメソッドが実行され、未初期化のフィールドへアクセスする可能性があるためです。

10-5. virtualメソッドを必ずoverrideする必要はある?

必要ありません。virtualはオーバーライドを許可する修飾子であり、強制する修飾子ではありません。

public class Parent{public virtual void Execute(){Console.WriteLine("既定の処理");}}

public class Child : Parent{}

ChildExecuteをオーバーライドしなければ、基底クラスの既定処理が使われます。

派生クラスに実装を強制したい場合は、abstractメソッドを使用します。

10-6. abstractとvirtualはどちらを使うべき?

基底クラスに妥当な既定処理があるかどうかで判断します。

既定処理を提供でき、必要な派生クラスだけが変更すればよい場合はvirtualが適しています。

public virtual void Log(string message){Console.WriteLine(message);}

基底クラスでは実装内容を決められず、すべての非抽象派生クラスに実装させたい場合はabstractが適しています。

public abstract decimal Calculate();

判断の目安は次のとおりです。

  • 既定の動作が自然に定義できるならvirtual

  • 派生クラスごとに必ず処理を決める必要があるならabstract

  • 派生クラスによる変更を許可しないなら通常メソッド

  • 継承関係が自然でないならインターフェースやコンポジションも検討する

10-7. newによるメソッド隠蔽はどのような場面で使う?

newは、基底クラスの同名メンバーを意図的に隠したい場合に使用します。

たとえば、外部ライブラリの基底クラスに後から同名メンバーが追加され、派生クラスの既存メンバーと衝突した場合に、隠蔽の意図を明示するために使うことがあります。

public class ExternalBase{public void Display(){Console.WriteLine("ExternalBase");}}

public class CustomClass : ExternalBase{public new void Display(){Console.WriteLine("CustomClass");}}

ただし、newでは呼び出し元の変数型によって実行結果が変わります。

CustomClass custom = new CustomClass();ExternalBase baseValue = custom;

custom.Display();baseValue.Display();

実行結果は次のとおりです。

CustomClassExternalBase

この動作は利用者を混乱させやすいため、通常の拡張設計ではvirtualoverrideを優先します。基底クラスを変更できず、隠蔽が意図したAPI設計である場合に限って慎重に使用するのが基本です。

まとめ

C#のvirtualは、基底クラスのメンバーを派生クラスで上書きできるようにする修飾子です。overrideは、基底クラスで上書き可能とされたメンバーを派生クラスで実際に上書きするために使用します。

基本形は次のとおりです。

public class Parent{public virtual void Execute(){Console.WriteLine("Parent");}}

public class Child : Parent{public override void Execute(){Console.WriteLine("Child");}}

基底クラス型の変数に派生クラスのインスタンスを代入しても、実際のインスタンスに対応した処理が呼び出されます。

Parent value = new Child();value.Execute();

実行結果はChildです。この仕組みにより、複数の派生クラスを共通の基底型として扱いながら、クラスごとに異なる振る舞いを実現できます。

一方、newはオーバーライドではなくメンバーの隠蔽です。overrideでは実際のインスタンス型によって呼び出し先が決まりますが、newでは変数の宣言型によって呼び出し先が決まります。

設計時には、次の点を意識することが重要です。

  • 共通の既定処理を変更可能にするならvirtual

  • 派生クラスで基底処理を上書きするならoverride

  • 基底処理も残したいならbase

  • 実装を必須にするならabstract

  • それ以上の上書きを禁止するならsealed override

  • 意図的に同名メンバーを隠す場合だけnew

  • 継承関係が自然でない場合はインターフェースやコンポジションを検討する

virtualを安易に付けるのではなく、派生クラスにどこまで変更を許可するかを明確にすることが大切です。virtualoverridenewの違いを正しく理解すれば、安全で拡張しやすいC#のクラス設計につなげられます。