オーバーライドと多様性

オーバーライドとは

派生クラスでは、基底クラスで定義されているメソッドを上書きして新しいものにすることができます。
これをオーバーライドといいます。
(override=上書き、優先)

オーバーロードと名前が似ていますが別物です。


class BaseClass
{
    //オーバーライドされるメソッド
    public virtual void TestMethod()
    {
        Console.WriteLine("BaseClass");
    }
}

class DerivedClass : BaseClass
{
    //基底クラスのTestMethodをオーバーライド
    public override void TestMethod()
    {
        Console.WriteLine("DerivedClass");
    }
}

static void Main(string[] args)
{
    DerivedClass dc = new DerivedClass();
    dc.TestMethod();
    Console.ReadLine();
}
DerivedClass

仮想メソッド

まず、基底クラスでオーバーライドされるメソッドをvirtualキーワードで定義します。
virtualがつけられたメソッドは仮想メソッドといいます。

次に、派生クラスで先ほどの仮想メソッドをoverrideキーワードでオーバーライドします。
オーバーライドしたメソッドは派生クラスの動作に置き換えられます。

オーバーライドできるのは基底クラスでvirtualキーワードを付けたメソッドのみです。

隠蔽と同じ?

サンプルコードの動作だけを見れば基底クラスのメンバーの隠蔽と同じです。
派生クラス内でbaseキーワードを使用すれば、基底クラスのメソッドを呼び出すこともできます。

オーバーライドは多様性に深く関係する機能です。
隠蔽とオーバーライドの違いを説明する前にアップキャストについての説明が必要なので、後述します。

とりあえず明確な違いは、シグネチャ(引数の数やデータ型)が異なる場合、隠蔽は単なるメソッドのオーバーロードになり、オーバーライドはエラーになる、という点です。


class BaseClass
{
    public void TestMethodA()
    { }

    public virtual void TestMethodB()
    { }
}

class DerivedClass : BaseClass
{
    //可能
    //ただのオーバーロード
    //ただしnewは必要ないという警告は出る
    public new void TestMethodA(int n)
    { }

    //エラー
    //そんなシグネチャのメソッドはない
    public override void TestMethodB(int n)
    { }
}

仮想メソッドのオーバーライドは必須ではない

virtualキーワードを付けた仮想メソッドは必ずしも派生クラスでオーバーライドしなければならないわけではありません。
オーバーライドしなければ通常のメソッドと同じように使用できます。

プロパティもオーバーライド可能

ここではメソッドで説明していますが、プロパティもオーバーライド可能です。
フィールドのオーバーライドはできません。

アップキャスト

派生クラスのインスタンスは基底クラスのインスタンスとして扱うことができます。


class BaseClass
{ }

class DerivedClass : BaseClass
{ }

static void Main(string[] args)
{
    //変数の型は基底クラスだが
    //派生クラスのインスタンスを代入している
    BaseClass bc = new DerivedClass();
}

「new DerivedClass()」で生成されるのは、当然ですが派生クラスであるDerivedClassクラスのインスタンスです。
それを、基底クラスのインスタンス変数に代入しています。

奇妙に見えるかもしれませんが、これは文法的に許されています。
派生クラスのインスタンスを基底クラスのインスタンスとして扱うことをアップキャストといいます。
基底クラスを継承関係の「上位」(親)、派生クラスを「下位」(子)とみて、下から上へのキャストなので「アップ」キャストというわけです。
(Up Cast)


class BaseClass
{
    private int privateNum_base;
    protected int protectedNum_base;
    public int PublicNum_base;

    private void PrivateMethod_Base() { }
    protected void ProtectedMethod_Base() { }
    public void PublicMethod_Base() { }
}

class DerivedClass : BaseClass
{
    private int privateNum_derive;
    protected int protectedNum_derive;
    public int PublicNum_derive;

    private void PrivateMethod_Derive() { }
    protected void ProtectedMethod_Derive() { }
    public void PublicMethod_Derive() { }
}

static void Main(string[] args)
{
    BaseClass bc = new DerivedClass();

    //アップキャストしたインスタンスからは
    //基底クラスのpublicメンバーしか見えない
    int num = bc.PublicNum_base;
    bc.PublicMethod_Base();
}

派生クラスは基底クラスの非privateメンバー(フィールド、メソッド、プロパティ)を全て持っています。
privateメンバーは持ちませんが、これが外部からは見えないのは基底クラスも派生クラスも同じことです。
つまり、派生クラスは(外から見れば)基底クラスの機能をすべて持っているのと同じことなので、基底クラスとして扱っても問題ないのです。

アップキャストとオーバーライド

アップキャストをするとオーバーライドに多様性を持たせることができます。


//人クラス
class Person
{
    public string Name { get; protected set; }
    public int Age { get; protected set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public virtual void Introduce()
    {
        Console.WriteLine(
            "私は{0}、{1}歳です。",
            Name, Age);
    }
}

//学生クラス
class Student : Person
{
    public int StudentNumber { get; protected set; }

    public Student(string name, int age, int studentNumber)
        : base(name, age)
    {
        StudentNumber = studentNumber;
    }

    public override void Introduce()
    {
        Console.WriteLine(
            "私は{0}、{1}歳、出席番号は{2}番です。",
            Name, Age, StudentNumber);
    }
}

//社員クラス
class Employee : Person
{
    public string Department { get; protected set; }

    public Employee(string name, int age, string department)
        : base(name, age)
    {
        Department = department;
    }

    public override void Introduce()
    {
        Console.WriteLine(
            "私は{0}、{1}歳、{2}所属です。",
            Name, Age, Department);
    }
}

static void Speak(Person person)
{
    person.Introduce();
}

static void Main(string[] args)
{
    Person person = new Person("A山B太", 20);
    Person student = new Student("C谷D男", 16, 10);
    Person employee = new Employee("E下F子", 24, "営業部");

    Speak(person);
    Speak(student);
    Speak(employee);

    Console.ReadLine();
}

Personクラスは「名前」と「年齢」だけを持つ基底クラスです。
Studentクラスは「出席番号」を持つPersonクラスからの派生クラスです。
Employeeクラスは「所属部署」を持つPersonクラスからの派生クラスです。

Speakメソッドはそれぞれの人物に自己紹介をしてもらうメソッドです。
それぞれのクラスで定義されているIntroduceメソッドを呼び出しているだけです。

ここで注目するのはSpeakメソッドの引数です。
引数の型はPersonクラスとなっており、メソッドの処理内でもPersonクラスのインスタンスのIntroduceメソッドを呼び出しています。

このコードの実行結果は以下になります。

私はA山B太、20歳です。
私はC谷D男、16歳、出席番号は10番です。
私はE下F子、24歳、営業部所属です。

派生クラスの場合、実際に呼び出されているのはPersonクラスのメソッドではなく派生クラスのメソッドであることがわかります。
インスタンスstudentはPersonクラスとして扱われていますが、実体はStudentクラスです。
つまりStudentクラスで定義されるメンバーも内部的には持っています。
アップキャストしたインスタンスの仮想メソッドが呼び出されると、オーバーライドしたメソッドが呼び出されます。
オーバーライドされていなければ基底クラスの仮想メソッドが呼ばれます。

Speakメソッドからすれば引数personが基底クラスなのか派生クラスなのかは関係なく、ただ同じ処理をするだけで違う結果を得ることができます。
これが多様性です。

なお、アップキャストは暗黙的に行われるので、インスタンス生成時(newしたとき)に基底クラスのインスタンスに格納しておく必要はなく、派生クラスのインスタンスをそのままSpeakメソッドの引数に指定しても構いません。
引数として渡されたときに暗黙的にアップキャストされます。


static void Speak(Person person)
{
    person.Introduce();
}

static void Main(string[] args)
{
    Person person = new Person("A山B太", 20);
    Student student = new Student("C谷D男", 16, 10);
    Employee employee = new Employee("E下F子", 24, "営業部");

    Speak(person);
    Speak(student);
    Speak(employee);
}

このコードの実行結果は上のコードと同じです。

隠蔽とオーバーライド

このような動作が可能なのはオーバーライドしたメソッドの場合であり、隠蔽したメソッドではできません。
これが隠蔽とオーバーライドの違いです。


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

class DerivedClass : BaseClass
{
    //隠蔽
    public new void MethodA()
    {
        Console.WriteLine("DerivedClass MethodA");
    }

    //オーバーライド
    public override void MethodB()
    {
        Console.WriteLine("DerivedClass MethodB");
    }
}

static void Speak(BaseClass bc)
{
    bc.MethodA();
    bc.MethodB();
}

static void Main(string[] args)
{
    BaseClass bc = new DerivedClass();
    Speak(bc);
    Console.ReadLine();
}
BaseClass MethodA
DerivedClass MethodB

メソッドの隠蔽のほうは基底クラスのメソッドが呼び出されてしまっていることがわかります。

ダウンキャスト

アップキャストがあるのだからダウンキャストもあります。
しかしダウンキャストは基本的に禁止されています。


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

class DerivedClass : BaseClass
{
    public int Num;
    public override void TestMethod() { }
}

static void Main(string[] args)
{
    //ダウンキャスト
    //エラー
    DerivedClass dc1 = new BaseClass();

    //明示的にキャストすれば文法上は可能
    //だが実行時エラー
    DerivedClass dc2 = (DerivedClass)new BaseClass();

    //dc2はNumを持っていない
    dc2.Num = 10;
}

基底クラスのインスタンスを派生クラスの変数に格納しても、実体は基底クラスですので派生クラスのメンバーは持っていません。
しかしダウンキャストすると派生クラスとして扱うので、存在しないメンバーへのアクセスが文法上可能になってしまいます。
存在しないものは使えないのでエラーになるのです。

例外的にダウンキャストができるケース

例外的にダウンキャストができるのは、アップキャストしたインスタンスをダウンキャストで元に戻す場合です。


static void Main(string[] args)
{
    DerivedClass dc1 = new DerivedClass();

    //アップキャスト
    BaseClass bcdc = dc1;

    //ダウンキャストで元に戻す
    DerivedClass dc2 = (DerivedClass)bcdc;
}

アップキャストされたインスタンスの中身は派生クラスなのだから、再度派生クラスとして扱っても問題はないということです。

インスタンスが指定のクラスにキャスト可能かはas演算子is演算子を使用します。


class Base
{ }

class DerivedA : Base
{ }

class DerivedB : Base
{ }

static void Main(string[] args)
{
    Base b1 = new DerivedA();
    Base b2 = new DerivedB();

    DerivedA d1 = b1 as DerivedA;
    DerivedA d2 = b2 as DerivedA;

    Console.WriteLine(
        "d1キャスト" + (d1 == null ? "失敗" : "成功"));
    Console.WriteLine(
        "d2キャスト" + (d2 == null ? "失敗" : "成功"));

    if(b1 is DerivedA)
        Console.WriteLine("b1はDerivedAにキャスト可能");
    else
        Console.WriteLine("b1はDerivedAにキャスト不可");

    if (b2 is DerivedA)
        Console.WriteLine("b2はDerivedAにキャスト可能");
    else
        Console.WriteLine("b2はDerivedAにキャスト不可");

    Console.ReadLine();
}
d1キャスト成功
d2キャスト失敗
b1はDerivedAにキャスト可能
b2はDerivedAにキャスト不可

例えばListクラスなどでインスタンスをまとめて扱う場合、基底クラスで受け取るようにすればすべての派生クラスをListクラスに格納できます。
(Addした時に暗黙的なアップキャストが行われる)
派生クラスの機能を使用する際はas演算子やis演算子で安全なダウンキャストをすれば良いというわけです。


class Base
{ }

class DerivedA : Base
{
    public void WriteLineA()
    {
        Console.WriteLine("DerivedA");
    }
}

class DerivedB : Base
{
    public void WriteLineB()
    {
        Console.WriteLine("DerivedB");
    }
}

static void Main(string[] args)
{
    List<Base> lst = new List<Base>();

    //このListは
    //Baseクラスの派生クラスも格納できる
    lst.Add(new Base());
    lst.Add(new DerivedA());
    lst.Add(new DerivedB());

    foreach(var e in lst)
    {
        if(e is DerivedA)
        {
            ((DerivedA)e).WriteLineA();
        }
        else if(e is DerivedB)
        {
            ((DerivedB)e).WriteLineB();
        }
    }

    Console.ReadLine();
}
DerivedA
DerivedB