[1] 継承とは?
継承というのは、クラスを拡張して新たなクラスを作る機能のことです。このとき、拡張される元のクラスを基底クラス。拡張して出来上がったクラスを派生クラスといいます。基底クラスをA、派生クラスをBとすると、次のようなコードになります。一つのクラスは、複数の基底クラスを持つことは出来ません。
このようにして定義されたクラスBは、クラスAと全く同じ操作も出来ます。では、サンプルコードを見てみましょう。
import std.stdio;
class Base{
int a = 1000;
public:
int getA(){
return a;
}
void setA(int value){
a = value;
}
}
//
class Derived : Base{
int b = 2000;
public:
int getB(){
return b;
}
void setB(int value){
b = value;
}
}
void main(){
Base base = new Base;
Derived derived = new Derived;
writefln("%d",base.getA());
base.setA(base.getA() * 3);
writefln("%d",base.getA());
writefln("%d",derived.getB());
derived.setB(base.getA() * 2);
writefln("%d",derived.getB());
/*基底クラスのメソッド呼び出し*/
writefln("%d",derived.getA()); //※1
}
1000
3000
2000
6000
1000
※のところに注目すれば分かりますが、このように、派生クラスのオブジェクトから、基底クラスの公開メンバを呼び出せることが分かると思います。
それと、アクセス指定子がprivateのメンバには、派生クラスからアクセスすることは出来ません。ですが、アクセス属性がprotectedである基底クラスのメンバには、派生クラスの内部からアクセスすることが出来ます。クラスの外からは、どちらにもアクセスすることは出来ません。
注意:同一モジュール(ソースファイル)からは、たとえprivateであっても(もちろんprotectedであっても)、全てのクラスメンバにアクセスすることが出来る。
[2] オーバーライド
派生クラスは、基底クラスと全く同じメンバを持つわけですから、当然、基底クラスとして扱うことが出来るわけです。実は、派生クラスの参照を、基底クラスの参照変数に、明示的な型変換をなくして代入することが出来ます。
import std.stdio;
class Base{
int a = 1000;
public:
int getA(){
return a;
}
void setA(int value){
a = value;
}
}
//
class Derived : Base{
int b = 2000;
public:
int getB(){
return b;
}
void setB(int value){
b = value;
}
}
void main(){
Base base = new Derived; //派生クラスのオブジェクトを基底クラスの参照に代入する。
writefln("%d",base.getA());
base.setA(base.getA() * 3);
writefln("%d",base.getA());
}
1000
3000
このように、基底クラスと、型システム上も同じように扱うことが出来てしまいます。それに加えて、派生クラスは、基底クラスのメソッドを上書きして、メソッドの振る舞いを入れ替えることが出来るのです。これを、「オーバーライド」と言います。オーバーライドのやり方は、単に、基底クラスで定義されているメソッドと、同じ引数リスト(型とその順序が完全に一致しなければならない)を持ち、基底クラスの戻り値型と一致するか、それの派生クラスを戻り値とするメンバ関数を定義することになります。派生クラスで上書きできる関数のことを、「仮想関数」と言います。
それと、privateな基底クラスのメンバ関数は、派生クラスで同名同シグネチャの関数を定義しても、オーバーライドになりません。全く別のメンバ関数として定義されます。final属性のメンバ関数は、派生クラスでのオーバーライドが禁止ということを意味します。
import std.stdio;
class Base{
int a = 100;
public:
void print(){
writefln("Called Base.print this = %p,a = %d",cast(void*)this,a);
}
int getA(){
return a;
}
void setA(int value){
a = value;
}
}
class Derived : Base{
public:
this(){
setA(200);
}
override void print(){
writefln("Called Derived.print this = %p,getA() = %d",cast(void*)this,getA());
}
}
void main(){
Base base = new Base, derived = new Derived; //derivedはBase型の変数であることに注意
writefln("base = %p,derived = %p",cast(void*)base,cast(void*)derived);
base.print();
derived.print();
}
base = B7574E50,derived = B7574E40
Called Base.print this = B7574E50,a = 100
Called Derived.print this = B7574E40,getA() = 200
このように、派生クラスのオブジェクトを基底クラスの参照変数によって扱っているのにもかかわらず、派生クラスのprintが呼ばれていることが分かります。そして、オーバーライドする関数には、override属性をつけることが出来ます(つけなくてもかまわないのですが…)。この場合、オーバーライドになっていなければ、コンパイルエラーになります。これは、オーバーライドしたつもりが… というバグを防止するのに有効な方法です。
[3] 抽象クラス・抽象メソッド
クラスにabstract属性を付加すると、そのクラスは抽象クラスになります。抽象クラスとは、そのクラス単体のオブジェクトが存在しないクラスのことです。つまり、必ず継承されなければ使われないクラスということになります。メソッドにもabstract属性をつけることが出来ます。この場合、メソッドの実装は省略することができますが、必ず派生クラスでオーバーライドされなければなりません。(実装を省略しないことが出来るのは、オーバーライド先で、基底クラスのメソッドに実装の一部を移譲できるようにするためです。) クラスがabstact属性を持つメソッドを持つ場合、そのクラスは抽象クラスとなり、直接のインスタンス生成は出来なくなります。
[4] インターフェイス
クラスがどのようなメソッドを持つべきかということについて、あらかじめ定義しておきたい場合があります。というのは、「あれとかこれとかそれとかをメソッドとして持っているクラス」を一元的に操作したいときがあります。具体的には、コールバックオブジェクトなどですね。
D言語では、このような目的のために、インターフェイスという言語機能を用意しています。インターフェイスは、実装を持たない仮想関数の宣言リストみたいなものですが、finalやstaticを属性として持っている関数の実装を書くことは出来ます。そして、全てのメンバがpublicとなります。
インターフェイスの宣言の仕方は このように、クラスを定義するときと同じように宣言します。インターフェイス名は、識別子の命名ルールに従います。
クラスは、インターフェイスで定義されている全ての仮想関数をオーバーライドする必要があります。そして、一つのクラスは、複数のインターフェイスを同時に実装することができます。(インターフェイスを継承することを、「実装する」と表現します。) インターフェイスを同時に実装する場合は、基底クラス名の後に、コンマで実装する全てのインターフェイスの名前を並べます。具体的には、次のようになります。
前の記事「D言語入門記事 ~ConstとImmutable(2)と、時々scope(1)~」 ←→ 次の記事「関数のオーバーロード集合」