トレイトの初期化処理で抽象 val にアクセスする

トレイトのメンバとして抽象 val を宣言した場合、初期化処理のタイミングでは、その val の値が null になっていることがあります。
以下の例では、Foo の初期化時には value2 が null になっています。

object Sample1 {
  def main(args: Array[String]) {
    val foo = new FooImpl
    println("[main]foo.value1 is "+foo.value1)
    println("[main]foo.value2 is "+foo.value2)
  }

  trait Foo {
    // value は実装クラスで設定する
    val value1: String
    val value2: String

    // Foo の初期化処理
    println("[foo]value1 is "+value1)
    println("[foo]value2 is "+value2)
  }

  class FooImpl extends Foo {
    val value1 = "I love Perfume!"
    val value2 = new String("I love Perfume!")
  }
}

実行結果は以下のとおりです。


[foo]value1 is I love Perfume!
[foo]value2 is null
[main]foo.value1 is I love Perfume!
[main]foo.value2 is I love Perfume!
インスタンス生成を伴う場合に、val の値が null になっているようです。

未初期化の場合に null となる振る舞いは抽象 val に限ったことではなく、単独のクラスでも同様です。

object Sample2 {
  def main(args: Array[String]) {
    val foo = new Foo
    println("[main]foo.value is "+foo.value)
  }

  class Foo {
    // value の初期化まえに参照している
    println("[foo]value is "+value)

    val value = "I love Perfume!"
  }
}

実行結果は以下のとおりです。


[foo]value is null
[main]foo.value is I love Perfume!

これらの振る舞いを回避するには、以下のような方法が考えられます。

  • val を参照している箇所よりも前に、val の定義を記述する
  • val に lazy をつける

Sample2 のような場合であれば val の定義を前方に移動する方法ですみます。しかし、Sample1 の場合は、親(Foo)→子(FooImpl)の順に初期化がなされるため、lazy をつけることで回避します。

object Sample3 {
  def main(args: Array[String]) {
    val foo = new FooImpl
    println("[main]foo.value is "+foo.value)
  }

  trait Foo {
    val value: String
    println("[foo]value is "+value)
  }

  class FooImpl extends Foo {
    // lazy をつけて定義する
    lazy val value = new String("I love Perfume!")
  }
}

実行結果は以下のとおりです。


[foo]value is I love Perfume!
[main]foo.value is I love Perfume!

まとめ

  • 初期化処理中はコンストラクタ引数以外の val は出来る限り参照しない
  • 参照する場合は
    1. val の初期化有無に注意
    2. 必要に応じて val に lazy をつける

いじょ。*1

追記

そもそも、コンストラクタ内からオーバーライド可能なメソッドを呼び出さないですむように、可能ならば設計を見直したほうがよいです*2Java(JVM?)の場合、親クラスのコンストラクタ内であっても、子クラスでオーバーライド/実装したメソッドを呼び出せるため*3 lazy をつけることで*4うまく動いていますが・・・

ちなみに、lazy をつけることで回避できるというのは、言語仕様の上で保証された振る舞いなのかあやしいです。たぶん実装依存な方法ではないかと・・・(汗

*1:lazy のコストが低い&すぐにインスタンス化されてなくて良いのであれば、val を定義するときは何も考えずに lazy val としても良いような気もします・・・

*2:def init のようなメソッドを用意し、必要なコンポーネントを一通り生成したあとに init を呼び出すとか。

*3:C++の場合は子クラスのコンストラクタ実行前はメソッドがオーバーライドされていない

*4:lazyをつけることで、プロパティにアクセスしたタイミングで初期化が実行される。lazy をつけていないときは、たぶんコンストラクタで初期化が実行されている。