virtual/overrideの基本

文法 難易度: ★☆☆☆☆(入門)

前提知識

先にこちらを読んでおくと、この記事がすらすら読めます。

継承(けいしょう)は、あるクラスの機能を引き継いで新しいクラスを作ることだ。継承元のクラスを基底クラス、継承して作った新しいクラスを派生クラスと呼ぶ。virtualは基底クラスのメソッドに付けて「派生クラスで上書きしてよい」と宣言するキーワードで、overrideは派生クラス側で実際に上書きするキーワードだ。

動かしてみる

Employee employee = new Manager();
Console.WriteLine(employee.Describe());

public class Employee
{
    public virtual string Describe() => "従業員";
}

public class Manager : Employee
{
    public override string Describe() => base.Describe() + "(マネージャー)";
}
従業員(マネージャー)

employee変数はEmployee型で宣言しているが、実際にnewしているのはManagerだ。Describe()を呼び出すと、変数の宣言型ではなく、実際に生成したインスタンスの型であるManageroverride済みの実装が実行される。Manageroverrideの中ではbase.Describe()と書くことで、上書きする前のEmployeeの実装も呼び出している。

最低限の理解

⚠️ overrideを付け忘れる

overrideを書かずに派生クラスへ同名のメソッドを定義すると、基底クラスのメソッドとは別物の新しいメソッドとして扱われる。これを隠蔽と呼ぶ。ビルドは通るが「override キーワードを追加してください」という警告(CS0114)が出て、呼び出し結果が変数の宣言型によって変わってしまう。

// NG: overrideを書き忘れると、基底クラスのメソッドとは別物として隠蔽される
Member member = new PremiumMember();
Console.WriteLine(member.GetDiscountRate());

public class Member
{
    public virtual double GetDiscountRate() => 0;
}

public class PremiumMember : Member
{
    public double GetDiscountRate() => 0.1;
}
0

member変数の宣言型はMemberなので、呼ばれるのはMember側のGetDiscountRate()であり0が出力される。PremiumMemberに定義したGetDiscountRate()は、警告が出るだけで上書きにはならない。

// OK: overrideを付けて実際に上書きする
Member member = new PremiumMember();
Console.WriteLine(member.GetDiscountRate());

public class Member
{
    public virtual double GetDiscountRate() => 0;
}

public class PremiumMember : Member
{
    public override double GetDiscountRate() => 0.1;
}
0.1

⚠️ コンストラクタからvirtualメソッドを呼ぶ

基底クラスのコンストラクタからvirtualメソッドを呼ぶと、その時点で実行されるのは派生クラス側のoverrideだ。ところが派生クラスのコンストラクタ本体はまだ実行されておらず、そこで代入するはずだったフィールドの値がまだ入っていない。

// NG: 基底クラスのコンストラクタ内でvirtualメソッドを呼んでいる
var manager = new Manager();

public class Employee
{
    public Employee() => Describe();
    public virtual void Describe() => Console.WriteLine("従業員");
}

public class Manager : Employee
{
    private string _department;
    public Manager()
    {
        _department = "営業部";
    }
    public override void Describe() => Console.WriteLine(_department);
}

Managerを生成すると、まず基底クラスであるEmployeeのコンストラクタが実行され、その中でDescribe()が呼ばれる。この時点ではManager自身のコンストラクタ本体はまだ実行されておらず、_departmentへの代入も済んでいないため、空の値のまま出力される。

// OK: コンストラクタからvirtualメソッドを呼ばず、生成が終わってから呼び出す
var manager = new Manager();
manager.Describe();

public class Employee
{
    public virtual void Describe() => Console.WriteLine("従業員");
}

public class Manager : Employee
{
    private string _department;
    public Manager()
    {
        _department = "営業部";
    }
    public override void Describe() => Console.WriteLine(_department);
}
営業部

課題

自分が扱うドメインで基底クラス1つと派生クラス2つを作り、共通のメソッドをvirtualにして、それぞれ異なる出力になることを確認する。次に、片方の派生クラスからoverrideを外してビルドし、どんな警告が出て呼び出し結果がどう変わるか確認する。

次に読む