オーバーライド
メソッドの「再定義」
派生クラスでは、基底クラスで定義されているメソッドを上書きして新しいものにすることができます。
これをオーバーライドといいます。
(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)
{ }
}
仮想メソッドのオーバーライドは必須ではない
仮想メソッドは必ずしも派生クラスでオーバーライドしなければならないわけではありません。
オーバーライドしなければ派生クラスでも通常のメソッドと同じように使用できます。
プロパティもオーバーライド可能
ここではメソッドで説明していますが、プロパティもオーバーライド可能です。
フィールドのオーバーライドはできません。
アップキャスト
派生クラスのインスタンスは基底クラスのインスタンスとして扱うことができます。
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は、Speakメソッド内ではPersonクラスとして扱われていますが、実体はStudentクラスです。
つまりStudentクラスで定義されるメンバーも内部的には持っています。
アップキャストしたインスタンスの仮想メソッドが呼び出されると、オーバーライドしたメソッドが呼び出されます。
オーバーライドされていなければ基底クラスの仮想メソッドが呼ばれます。
Speakメソッドでは、引数personの実体が基底クラスなのか派生クラスなのかを区別する必要はなく、同じ処理をするだけで違う結果を得ることができます。
つまり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クラスに格納できます。
(Listに追加した時に暗黙的なアップキャストが行われる)
派生クラスの機能を使用する際はas演算子やis演算子で安全なダウンキャストをすれば良いというわけです。
struct Point
{
public int x;
public int y;
}
//形状クラス
class Shape
{
public string Name;
public Point Point; //座標
public void PrintPoint()
{
Console.WriteLine("座標: {0}, {1}",
Point.x, Point.y);
}
}
//円クラス
class Circle : Shape
{
public int Radius; //半径
public void PrintArea()
{
Console.WriteLine("面積: {0}",
(int)(Radius * Radius * 3.14));
}
}
static void Main(string[] args)
{
//このListは
//Shapeクラスの派生クラスも格納できる
List<Shape> lst = new List<Shape>();
lst.Add(new Shape {
Name = "Shape1",
Point = { x = 10, y = 20 } });
lst.Add(new Circle {
Name = "Circle1",
Point = { x = 30, y = 40 },
Radius = 5
});
lst.Add(new Circle {
Name = "Circle2",
Point = { x = 50, y = 60 },
Radius = 3
});
lst.Add(new Shape {
Name = "Shape2",
Point = { x = 70, y = 80 }
});
foreach (var e in lst)
{
Console.WriteLine("{0}:", e.Name);
e.PrintPoint();
if(e is Circle)
{
//ダウンキャスト
((Circle)e).PrintArea();
}
Console.WriteLine();
}
Console.ReadLine();
}
Shape1: 座標: 10, 20 Circle1: 座標: 30, 40 面積: 78 Circle2: 座標: 50, 60 面積: 28 Shape2: 座標: 70, 80