trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

EFFECTIVE C# 6.0/7.0 読書メモ 項目16

項目16 コンストラクタ内では仮想メソッドを呼ばないこと

コンストラクタで仮想メソッドを呼び出したときの動作について。

class B
{
    protected B()
    {VFunc();}
    protected virtual void VFunc()
    {Console.WriteLine("B内のVFunc");}
}
class Derived :B
{
    private readonly string msg = "初期化子で設定";
    public Derived(string msg)
    {this.msg = msg;}
    protected override void VFunc()
    {Console.WriteLine(msg);}
}

上記のような、コンストラクタで仮想メソッドを呼び出すクラスBから派生したクラスDerivedを作成すると、

> var d = new Derived("メイン内のコンストラクタ");
初期化子で設定

となる。

Dereivedを作成したとき、

  1. msgを初期化子で設定(msg = "初期化子で設定")
  2. 親クラスBのコンストラクタ実行(DerivedのVFunc())
  3. Derivedのコンストラクタ実行(this.msg = msg)

の順に実行される。

説明
親クラスのコンストラクタでは実行時におけるオブジェクトでオーバーライドされた仮想メソッドを呼び出す。上記の例では、 作成対象となっているオブジェクトはDerived。つまり実行時におけるオブジェクトの型はDerivedなのでDerivedのオーバーライドメソッド(VFunc)が呼ばれる。

このとき、Derivedのコンストラクタによる初期化が行われる前なのでmsgには初期化子で設定された値が入っている。

問題点
コンストラクタで仮想メソッドを呼び出すようにすると、 派生クラスのコンストラクタで設定される前の値が使われる。 派生クラスにおけるすべてのメンバ変数は初期化子あるいはシステムによって設定された初期状態のまま。

この不安定さを回避するには、派生クラスではすべてのインスタンス変数を初期化子で初期化する、派生クラスではデフォルトコンスタラクタを定義しそれ以外のコンストラクタを作成させない といった制限が必要になる。