目次

概要

継承構造を持つクラスのコンストラクターの挙動と注意点の話を少々。

コンストラクターの実行順序

派生クラスのインスタンスが生成される際、 派生クラスのコンストラクターの前に、基底クラスのコンストラクターが呼び出されます。

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# では、仮想メソッドテーブルの更新だけは先にして、 それから基底クラスのコンストラクター → 派生クラスのコンストラクターの順で処理が行われます。

余談: 実行順序に関して

コンストラクター初期化子」で説明したように、 初期化の順序は、メンバー変数初期化子 → コンストラクター初期化子 → コンストラクター本体の順序になります。

基底クラスのコンストラクター呼び出しは、この2つ目、コンストラクター初期化子のところに入ります。 以下のようなコードを書くと実行順序がはっきりします。


class Member
{
    public Member(string s)
    {
        Console.Write("Member {0}\n", s);
    }
}

class Base
{
    Member x = new Member("base");

    public Base()
    {
        Console.Write("Base()\n");
    }
}

class Derived : Base
{
    Member x = new Member("derived");

    public Derived()
    {
        Console.Write("Derived()\n");
    }
}

class Program
{
    static void Main()
    {
        new Derived();
    }
}
Member derived
Member base
Base()
Derived()

で、メンバー変数初期化子を使って値を設定した変数は、 基底クラスのコンストラクターが呼ばれた時点ですでにきちんと初期化済みな事が保証されます。 基底クラスから仮想メソッドを呼び出す場合、 このことに留意してコードを書くとトラブルになりにくいです。

コンストラクター中での仮想メソッド呼び出しの問題点

「それでいいじゃないか、仮想メソッドってのは動的な型に基づいて行われるんだから、 これが期待通りの動作じゃないの?」と思うかもしれません。

まあ、それはそうなんですが、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。)

更新履歴

ブログ