初期化失敗時にnullになるとは??
cake patternを利用したDI時などで定数が束縛される前にその定数を呼び出すとnullになります。1
これはScalaを始めた頃によく陥る罠2 で私も昔これでよく悩みました。
最近友人とこの話題になり、なぜnullになるのか疑問に思い調査してみました。
環境
調査
再現する例を示す
class Example {
println(s)
val s = "hello world"
println(s)
}
classインスタンスを生成したいのでREPLで実行する
$ scala
Welcome to Scala 2.13.4 (Eclipse OpenJ9 VM, Java 1.8.0_282).
Type in expressions for evaluation. Or try :help.
scala> class Example {
| println(s)
| val s = "hello world"
| println(s)
| }
|
println(s)
^
On line 2: warning: Reference to uninitialized value s
class Example
scala> new Example
null
hello world
val res0: Example = Example@9a44ab10
定数sを束縛する前に呼び出すと確かにnullになる
束縛する前に値を呼び出せるのがそもそもよくわからないが、インスタンスが作られていない状態で呼び出すとnullになるという挙動がよくわからない
たとえば当たり前だがjavaだとコンパイル時にcannot find symbol
でエラーになる
class Example {
Example(){
System.out.println(s);
final String s = "hello java";
System.out.println(s);
}
}
$ javac example.java
example.java:3: エラー: シンボルを見つけられません
System.out.println(s);
^
シンボル: 変数 s
場所: クラス Example
エラー1個
つまり String型の挙動ではない
Scalaのコンパイルフェーズでconstructorsというものがあり、フィールド定義をコンストラクタへ移動するということを行う
このコンパイラフェーズでコンストラクタに持ち上げる時にnullを入れているのだと予想した
scalaコンパイラはこのコンパイルフェーズの途中のものを出力することができる
$ scalac example.scala -Xprint:constructors
example.scala:2: warning: Reference to uninitialized value s
println(s)
^
[[syntax trees at end of constructors]] // example.scala
package {
class Example extends Object {
private[this] val s: String = _;
def s(): String = Example.this.s;
def (): Example = {
Example.super.();
scala.Predef.println(Example.this.s());
Example.this.s = "hello world";
scala.Predef.println(Example.this.s());
()
}
}
}
1 warning
private[this] val s: String = _;
予想したとおりsに_
という初期値を代入している箇所を見つけた
この_
は
に書いてあるとおり
var かつ 型アノテーションがついている場合のみ利用できる
_
はTTの型によって値が変わり、AnyRefを継承していた場合はnull、AnyValを継承していた場合は0っぽい値が代入される
結果
AnyRefを継承している型をインスタンス化せずに呼び出そうとするとコンパイルフェーズ18のconstructorsの際に初期値として代入された_
が呼び出され、その値のnullになる
感想
業務や趣味でScalaのコンパイルフェーズを一つづつ表示していたのでコンパイルフェーズ18でなにかやっているのではないか?`という仮定にたどり着けたのだと思う
またDIなどをするときはほとんどが独自型なのでnull
になるが、AnyVal(Int, Long, etc..)を束縛前に利用すると0っぽい値が入り、実行時にも中々気が付けない場合があると考えると結構怖い
scala3(dotty)の話
Scala3ではuninitialized
というメソッドに変えられている(コンパイルフェーズ中にしか利用できないメソッドになっている)
https://dotty.epfl.ch/api/scala/compiletime.html#uninitialized-0
突然の便乗したScala 3紹介としては、 _ の使い過ぎを多少反省したのか、専用のhttps://t.co/2L98tUJQjK
— Kenji Yoshida (@xuwei_k) February 24, 2021
uninitialized
という特別扱いメソッドが導入されています
追記1
nullになる場合はlazy valなどを使って回避しましょう
追記2
記事中のScalaとJavaの比較の箇所が正しく比較できていないようです
ちょっと僕には難しすぎて正しく解説できないので吉田さんのツイートをそのまま貼っておきます
https://t.co/m0WgVJFDom
— Kenji Yoshida (@xuwei_k) March 12, 2021
セマンティクス的にも、少なくともこの例では動作が同じになるという点でも、そもそもこうやって捉えれば、結局JavaもScalaも同じでは?https://t.co/hcec7KtXDV
とまで書いて気がついたけれど、fieldをfinalにしたらJavaではnullにならない?ので同じではなかった・・・
https://gist.github.com/xuwei-k/8c8baf19f4067ca465023c44254b6973