来源:https ://github.com/paulp/scala-faq/wiki/Initialization-Order
(感谢 Ghik - 支持评论或接受的答案而不是这个答案 - 为了完整性和历史记录,这里复制了此内容)。
初始化顺序 为什么我的抽象或覆盖的 val 为空?
考虑以下。
abstract class A {
val x1: String
val x2: String = "mom"
println("A: " + x1 + ", " + x2)
}
class B extends A {
val x1: String = "hello"
println("B: " + x1 + ", " + x2)
}
class C extends B {
override val x2: String = "dad"
println("C: " + x1 + ", " + x2)
}
// scala> new C
// A: null, null
// B: hello, null
// C: hello, dad
“严格”或“急切”的 val 是没有标记为惰性的。
在没有“早期定义”(见下文)的情况下,严格 val 的初始化按以下顺序完成。
超类在子类之前完全初始化。否则,按申报顺序。自然,当一个 val 被覆盖时,它不会被多次初始化。因此,尽管上面示例中的 x2 似乎在每个点上都定义了,但情况并非如此:在超类的构造过程中,被覆盖的 val 将显示为 null,抽象 val 也是如此。
有一个编译器标志可用于识别这种情况:
-Xcheckinit:向字段访问器添加运行时检查。
在测试之外使用这个标志是不可取的。它通过在所有可能未初始化的字段访问周围放置一个包装器来显着增加代码大小:包装器将抛出异常而不是允许空值(或在原始类型的情况下为 0/false)静默出现。另请注意,这会添加运行时检查:它只能告诉您有关您使用它执行的代码路径的任何信息。
在开场示例中使用它:
% scalac -Xcheckinit a.scala
% scala -e 'new C'
scala.UninitializedFieldError: Uninitialized field: a.scala: 13
at C.x2(a.scala:13)
at A.<init>(a.scala:5)
at B.<init>(a.scala:7)
at C.<init>(a.scala:12)
避免空值的方法包括:
使用惰性值。
abstract class A {
val x1: String
lazy val x2: String = "mom"
println("A: " + x1 + ", " + x2)
}
class B extends A {
lazy val x1: String = "hello"
println("B: " + x1 + ", " + x2)
}
class C extends B {
override lazy val x2: String = "dad"
println("C: " + x1 + ", " + x2)
}
// scala> new C
// A: hello, dad
// B: hello, dad
// C: hello, dad
通常是最好的答案。不幸的是,你不能声明一个抽象的惰性 val。如果这是您所追求的,您的选择包括:
声明一个抽象的 strict val,并希望子类将其实现为惰性 val 或具有早期定义。如果他们不这样做,它似乎在构建过程中的某些时候未初始化。声明一个抽象 def,并希望子类将其实现为惰性 val。如果他们不这样做,它将在每次访问时重新评估。声明一个抛出异常的具体惰性 val,并希望子类覆盖它。如果他们不这样做,它将...抛出异常。惰性 val 初始化期间的异常将导致在下一次访问时重新评估右侧:请参阅 SLS 5.2。
请注意,使用多个惰性 val 会产生新的风险:惰性 val 之间的循环可能会导致首次访问时堆栈溢出。
使用早期定义。
abstract class A {
val x1: String
val x2: String = "mom"
println("A: " + x1 + ", " + x2)
}
class B extends {
val x1: String = "hello"
} with A {
println("B: " + x1 + ", " + x2)
}
class C extends {
override val x2: String = "dad"
} with B {
println("C: " + x1 + ", " + x2)
}
// scala> new C
// A: hello, dad
// B: hello, dad
// C: hello, dad
早期定义有点笨拙,对于早期定义块中可以出现的内容和可以引用的内容存在限制,并且它们的组合不如惰性 val:但是如果不需要惰性 val,它们会提供另一种选择. 它们在 SLS 5.1.6 中指定。
使用常量值定义。
abstract class A {
val x1: String
val x2: String = "mom"
println("A: " + x1 + ", " + x2)
}
class B extends A {
val x1: String = "hello"
final val x3 = "goodbye"
println("B: " + x1 + ", " + x2)
}
class C extends B {
override val x2: String = "dad"
println("C: " + x1 + ", " + x2)
}
abstract class D {
val c: C
val x3 = c.x3 // no exceptions!
println("D: " + c + " but " + x3)
}
class E extends D {
val c = new C
println(s"E: ${c.x1}, ${c.x2}, and $x3...")
}
//scala> new E
//D: null but goodbye
//A: null, null
//B: hello, null
//C: hello, dad
//E: hello, dad, and goodbye...
有时,您从接口中需要的只是编译时常量。常量值比严格的更严格,比早期的定义更早,并且有更多的限制,因为它们必须是常量。它们在 SLS 4.1 中指定。