scala/ハマりどころ
#scala
ドキュメントにコンパイラオプションが網羅されていないがち
https://docs.scala-lang.org/scala3/reference/other-new-features/safe-initialization.html とかはサラッとページを立てて解説されているが、他でこのオプションについて触れてる箇所がない気がする
objectではvalのforward referenceができてしまう
以下のようなコードについて考える。
code:scala
def f =
val x = y // y is a forward reference extending over the definition of x
val y = 42
println(x)
当然これはコンパイルが通らない。xの定義のタイミングでyは存在しないため。
しかしobjectだとこれができてしまう:
code:scala
object O:
val x = y
val y = 42
O.x // => 0
プリミティブ型ではないときはnullになるので、ぬるぽの原因になる
objectの仕様 https://www.scala-lang.org/files/archive/spec/3.4/05-classes-and-objects.html#object-definitions
Note that the value defined by an object definition is instantiated lazily. The new m$cls constructor is evaluated not at the point of the object definition, but is instead evaluated the first time m is dereferenced during execution of the program (which might be never at all). An attempt to dereference m again during evaluation of the constructor will lead to an infinite loop or run-time error. Other threads trying to dereference m while the constructor is being evaluated block until evaluation is complete.
(機械翻訳)
オブジェクト定義によって定義された値は、遅延初期化される点に注意してください。新しい m$cls コンストラクタは、オブジェクト定義の時点では評価されず、プログラム実行時に m が初めて参照された時点で初めて評価されます(場合によっては全く評価されないこともあります)。コンストラクタ評価中に再度 m を参照しようとすると、無限ループや実行時エラーが発生します。コンストラクタ評価中に、他のスレッドが m を参照しようとした場合、評価が完了するまで待機状態になります。
disasmした結果を見るとわかるが、
objectのフィールドはJVMレベルではprivate static finalのフィールドとして定義される
初期状態では、プリミティブ型は0やfalseで初期化される
これはJavaの仕様
で、x=yという初期化がさらに行われる
yはまだ0扱いなのでx=0になる
次にy=42になる
回避策
コンパイラオプションに-Wsafe-initをつける(Scala 3の場合)と、コンパイラが警告するようになる
code:scala
warn ./code.scala.sc:6:7
warn Access non-initialized value y. Calling trace:
warn ├── object O: code.scala.scala:10
warn │ ^
warn └── val x = y code.scala.scala:11
warn ^
warn val y = 42
warn ^
どっちかをlazy valにすると後から初期化されるようになるので値が正常になる
----
このコードをdisasmすると:
code:java
public final class O {
public static int x();
0: getstatic #13 // Field O$.MODULE$:LO$;
3: invokevirtual #15 // Method O$.x:()I
6: ireturn
public final class O$ implements java.io.Serializable {
private static final int x;
private static final int y;
public static final O$ MODULE$;
private O$();
0: aload_0
1: invokespecial #16 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LO$;
public static {};
0: new #2 // class O$
3: dup
4: invokespecial #19 // Method "<init>":()V
7: putstatic #21 // Field MODULE$:LO$;
10: getstatic #21 // Field MODULE$:LO$;
13: invokevirtual #24 // Method y:()I
16: putstatic #26 // Field x:I
19: bipush 42
21: putstatic #28 // Field y:I
24: return
private java.lang.Object writeReplace();
0: new #32 // class scala/runtime/ModuleSerializationProxy
3: dup
4: ldc #2 // class O$
6: invokespecial #35 // Method scala/runtime/ModuleSerializationProxy."<init>":(Ljava/lang/Class;)V
9: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LO$;
public int x();
0: getstatic #26 // Field x:I
3: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LO$;
public int y();
0: getstatic #28 // Field y:I
3: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LO$;
}