継承構造を持つクラスのコンストラクタの挙動と注意点の話を少々。
派生クラスのインスタンスが生成される際、 派生クラスのコンストラクタの前に、基底クラスのコンストラクタが呼び出されます。
class B { public B() { Console.Write("base\n"); } } class D : B { public D() { Console.Write("derived\n"); } } class Program { static void Main(string[] args) { D d = new D(); } }
base derived
なので、派生クラスのコンストラクタ内では、 基底クラスのメンバはちゃんと初期化済みだと思って使えます。
class B { public double x; public B() { this.x = 5; } } class D : B { public double y; public D() { // ↓ ちゃんと y == 25 になる。 this.y = this.x * this.x; } } class Program { static void Main(string[] args) { D d = new D(); Console.Write(d.y); } }
25
「派生クラスのコンストラクタの前に基底クラスのコンストラクタが呼ばれる」というルールは、 たいていどの言語でも同じルールです。 C++ でも Java でもそういうルールでコンストラクタを呼び出します。
でも、1つだけ注意すべき点があります。 コンストラクタ中の仮想メソッド呼び出しの扱いに関して。 例えば、以下のような感じ。
class B { public B() { Console.Write(this.Name()); } public virtual string Name() { return "base"; } } class D : B { public D() { } public override string Name() { return "derived"; } } class Program { static void Main(string[] args) { D d = new D(); } }
この挙動は C++ と C# で違います。 C++ で、このコードに相当するものを書いて実行すると、 base と表示されます。 派生クラス D のインスタンスを生成しているにもかかわらず、 基底クラス B の Name メソッドが呼ばれます。
base
一方、C# では、以下のように、派生クラスの Name メソッドが呼ばれます。
derived
仮想メソッド(あるいは、C++ では仮想関数と呼ぶ)の呼び出しは、 仮想メソッドテーブル(仮想関数テーブル)というものを通して行います。 Name というメソッドが呼ばれたときに、 実際にはどのメソッド(D.Name なのか B.Name なのか)を呼べばいいか、 テーブル中に参照情報が書かれていて、 それを見て実際のメソッド呼び出しが行われます。
で、C++ では、コンストラクタの頭で仮想関数テーブルが更新されます。 基底クラスのコンストラクタ内では、まだ仮想関数テーブルが派生クラスのものに更新されていません。
一方、C# では、仮想メソッドテーブルの更新だけは先にして、 それから基底クラスのコンストラクタ → 派生クラスのコンストラクタの順で処理が行われます。
「それでいいじゃないか、仮想メソッドってのは動的な型に基づいて行われるんだから、 これが期待通りの動作じゃないの?」と思うかもしれません。
まあ、それはそうなんですが、1つ問題があります。 基底関数のコンストラクタ内で仮想メソッドが呼ばれた時点では、 派生クラスのメンバ変数は初期化されていない(派生クラスのコンストラクタはまだ呼ばれてない)んですよね。 例えば、以下のコードを見てください。
class B { public B() { Console.Write(this.Name()); } public virtual string Name() { return "anonymous"; } } class D : B { string name; public D(string name) { this.name = name; } public override string Name() { return this.name; } } class Program { static void Main(string[] args) { D d = new D("derived"); } }
前節の内容と比べて何が違うかというと、D.Name メソッド内で派生クラスのメンバ変数である name の値を読み出しています。
B のコンストラクタ内で Name メソッドが呼ばれた時点では、 まだ D のコンストラクタは実行されていません。 したがって、name 変数はまだ初期化されていない(null になっている)状態で、 結局、このコードの実行結果は何も出力されません。
C# では、コンストラクタ内での仮想メソッド呼び出しは、 動的な型に基づいて呼び出されます。
ただし、メンバ変数を読み出すような仮想メソッドをコンストラクタ内から呼び出すと、 正しい値が読めないので注意が必要です。 (メンバ変数にアクセスしない場合や、値を書き込む方は OK。)