トレイトの初期化処理で抽象 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!") } }
実行結果は以下のとおりです。
インスタンス生成を伴う場合に、val の値が null になっているようです。
[foo]value1 is I love Perfume!
[foo]value2 is null
[main]foo.value1 is I love Perfume!
[main]foo.value2 is I love Perfume!
未初期化の場合に 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!