新しいクラスのインスタンスが生成されようとしているとき、クラスのフィールドはその型の初期値で初期化されようとします。 整数型であれば 0、浮動小数点数型であれば NAN、 クラス型であれば null という具合に初期化されるでしょう。
フィールドの初期値を独自に設定したい場合、フィールド宣言と同時に変数を初期化することで明示的に値を指定することができます。
import std.stream; class Point { int x , y; } class Size { int width = 400, height = 300; } int main() { Point point = new Point; Size size = new Size; stdout.printf("point = %d, %d\n" , point.x , point.y); stdout.printf("size = %d, %d\n" , size.width , size.height); return 0; }
このプログラムの Point クラスの整数型フィールドは、インスタンス生成時に初期化していないため、初期値 0 が格納されています。 一方 Size クラスの整数型フィールドや変数初期化子を記述しているため、インスタンス生成時にフィールドが自動的に初期化されます。 実行結果を見れば、size オブジェクトはインスタンス生成以降なんの操作も行っていませんが、フィールドには明示的に初期化した値が格納されていることを確認できます。
フィールドの初期化は、初期化を忘れたときや省略した時に生じる単純ミスやバグを未然に防ぐことができます。 ただし、フィールドの初期化は必ず定数で行わなければならず、定数以外のコードを代入することはできません。
クラス型のフィールドを宣言するクラスなどでは、何らかのインスタンスを生成するなど、より具体的で、動的な初期化が必要になります。 そこで、クラスの初期化にはコンストラクタが用いられます。
コンストラクタとは特殊なメンバ関数のことで、new 演算子によってクラスのインスタンスが生成された時に、自動的に呼び出される関数です。 コンストラクタはインスタンス生成時に自動的に呼び出されるため、コンストラクタを定義すれば、フィールドの初期化をうっかり忘れてしまうというミスを確実に回避することができるようになります。
コンストラクタを定義するには、戻り値型を持たない this という名前のメンバ関数を定義します。 コンストラクタに戻り値がないのは、コンストラクタの戻り値は常にコンストラクタが定義されているクラスのオブジェクトだからです。
class クラス名 { this(仮引数リスト ...) { コンストラクタ本体 ... } }
C++、Java、C# 等の主要なオブジェクト指向型言語では、コンストラクタは戻り値型を持たないクラスと同じ名前のメソッドでしたが、D 言語では this という名前のメンバ関数がコンストラクタであることに注意してください。
コンストラクタの本体には、通常のメンバ関数と同様に必要な任意のコードを記述することができます。 ここに、クラスを初期化するために必要なプログラムを書くことで、インスタンス生成と同時に確実にオブジェクトを初期化することができます。
import std.stream; class Point { int x , y; } class Size { int width = 400 , height = 300; } class Rectangle { Point point; Size size; this() { stdout.writeLine("Rectangle のコンストラクタを実行しています"); this.point = new Point; this.size = new Size; } } int main() { Rectangle rect = new Rectangle; stdout.printf("rect = %d, %d, %d, %d\n" , rect.point.x , rect.point.y , rect.size.width , rect.size.height ); return 0; }
このプログラムの Rectangle クラスは、フィールドのクラス型の参照変数を保有しています。 これらのオブジェクトを適切に初期化するには、インスタンスを生成しなければなりません。 そこで、Rectangle クラスはコンストラクタを明示的に定義して、コンストラクタでフィールドを適切に初期化しています。 そのため、main() 関数で Rectangle クラスのインスタンスを生成した後は、Rectangle クラスのフィールドを初期化することなく、そのまま point 及び size オブジェクトを使用することができます。
上記のプログラムでは、コンストラクタのプログラムがインスタンス生成時に実行されていることを視覚的に確認できるように、標準出力にコンストラクタが実行されていることを表す文字列を出力していますが、通常はクラスの初期化以外のプログラムを記述するべきではありません。
コンストラクタは、インスタンスの初期化に必要な情報を引数として受け取ることができます。 この場合も、通常の関数と同じように、受け取る値を表す仮引数リストを定義するだけです。
コンストラクタが引数を要求している場合、new 演算子でインスタンスを生成するときに、プログラムはコンストラクタに必要な情報を渡さなくてはなりません。 これまでは、new 演算子の直後にクラス名を指定しているだけでしたが、引数を渡すにはクラス名に続いて引数を指定しなければなりません。
new クラス名 (引数 ...);
このインスタンスの生成方法は、関数の呼び出しと同じです。 これまでのインスタンスの生成はクラス名だけを指定してきましたが、これらも () を省略した形式に過ぎません。 例えば、引数を受け取らないコンストラクタを定義している Point クラスのインスタンスを生成する場合、次のように記述することもできます。
Point pt = new Point();
Java や C# 系の出身者であれば、この書き方に慣れているでしょう。
import std.stream; class Point { int x , y; this(int x , int y) { this.x = x; this.y = y; } } int main() { Point point = new Point(100 , 70); stdout.printf("point = %d, %d\n" , point.x , point.y); return 0; }
このプログラムの Point クラスは、フィールドを初期化するために 2 つの整数型を引数から受け取ります。 Point クラスのインスタンスを生成するには、コンストラクタを呼び出すために引数を指定しなければなりません。 実行結果を見れば、new 演算子でインスタンスを生成する時に指定した値で、適切に初期化されていることを確認することができます。
因みに、これまでのコンストラクタを明示的に定義しないクラスの場合は、引数を受け取らない、何もコードを実行しない空のコンストラクタが暗黙的に定義されていたと考えることができます。 引数を受け取らないコンストラクタは、次のように明示的に定義することもできます。
this() { ... }