C# の継承 - メソッドのオーバーライドと virtual・override の使い方

継承の大きな利点のひとつは、親クラスで定義されたメソッドの振る舞いを子クラスで変更できることです。C# ではこの仕組みを「オーバーライド」と呼び、virtualoverride という 2 つのキーワードを組み合わせて実現します。

virtual と override の基本

親クラス側でメソッドに virtual を付けると「このメソッドは子クラスで上書きしてよい」という宣言になります。子クラス側では override を付けて、実際に振る舞いを再定義します。

class Shape
{
    public virtual double GetArea()
    {
        return 0;
    }
}

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

    public Circle(double radius)
    {
        Radius = radius;
    }

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

Shape クラスの GetAreavirtual なので、Circle クラスが override で独自の面積計算に差し替えています。virtual が付いていないメソッドに対して override を書くとコンパイルエラーになるため、両方のキーワードが揃っている必要があります。

オーバーライドが呼ばれる仕組み

オーバーライドの重要な特徴は、変数の型ではなく実際のインスタンスの型に基づいてメソッドが選択される点です。

Shape shape = new Circle(5);
Console.WriteLine(shape.GetArea());

変数 shape の型は Shape ですが、実体は Circle のインスタンスです。この場合、CircleGetArea が呼ばれ、78.54... のような値が出力されます。これがポリモーフィズム(多態性)と呼ばれる動作で、継承とオーバーライドによって実現されています。

この「実行時に実際の型のメソッドが呼ばれる」仕組みを仮想メソッドディスパッチと呼びます。

コンパイル時ではなく実行時にどのメソッドを呼ぶか決定する仕組み。C# の virtual メソッドは内部的に仮想メソッドテーブル(vtable)を通じて解決される。

virtual を付けなかった場合

virtual を付けないメソッドは、子クラスでオーバーライドできません。同名のメソッドを子クラスで定義すると、コンパイラから警告が出ます。

class Shape
{
    public double GetArea()
    {
        return 0;
    }
}

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

    public Circle(double radius)
    {
        Radius = radius;
    }

    public new double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

new キーワードを使えば警告は消えますが、これはオーバーライドではなく「メソッドの隠蔽(シャドーイング)」です。ポリモーフィズムが機能しないため、変数の型によって呼ばれるメソッドが変わってしまいます。

override(オーバーライド)

実行時のインスタンスの型に基づいてメソッドが選ばれる。ポリモーフィズムが機能する。

new(シャドーイング)

コンパイル時の変数の型に基づいてメソッドが選ばれる。ポリモーフィズムは機能しない。

この違いはバグの原因になりやすいため、意図的にシャドーイングが必要な場面を除き、virtual / override の組み合わせを使うのが一般的です。

override したメソッドをさらに継承する

オーバーライドしたメソッドは、デフォルトでさらに下の子クラスからもオーバーライドできます。

class Shape
{
    public virtual string Describe()
    {
        return "図形";
    }
}

class Polygon : Shape
{
    public override string Describe()
    {
        return "多角形";
    }
}

class Triangle : Polygon
{
    public override string Describe()
    {
        return "三角形";
    }
}

ShapePolygonTriangle と 3 段階の継承があり、それぞれ Describe をオーバーライドしています。Triangle のインスタンスで Describe を呼べば「三角形」が返ります。

途中の階層でオーバーライドを止めたい場合は、sealed override と書くことでそのメソッドをそれ以上オーバーライドできなくなります。

class Polygon : Shape
{
    public sealed override string Describe()
    {
        return "多角形";
    }
}

こうすると TriangleDescribe をオーバーライドしようとした時点でコンパイルエラーになります。設計上、これ以上の変更を許可したくないメソッドに対して sealed を付けると、意図しない動作の上書きを防げます。

戻り値の型とオーバーライド

C# のオーバーライドでは、メソッドのシグネチャ(メソッド名・引数の型と数・戻り値の型)が親クラスと完全に一致している必要があります。戻り値の型だけを変えたり、引数を追加したりするとオーバーライドとして認識されません。

virtual メソッドをオーバーライドする際、正しい記述はどれですか?

  • public new double GetArea() { ... }
  • public override double GetArea() { ... }
  • public virtual double GetArea() { ... }
__RESULT__

new はシャドーイングであり、オーバーライドではありません。子クラスで振る舞いを上書きするには override を使います。virtual は親クラス側で宣言するキーワードです。