FAQ

为什么我的抽象或重写的 val 为 null?

语言

示例

为了理解该问题,我们选择以下具体示例。

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 REPL 观察初始化顺序

scala> new C
A: null, null
B: hello, null
C: hello, dad

只有当我们进入 C 的构造函数时,x1x2 才被初始化。因此,AB 的构造函数有遇到 NullPointerException 的风险。

说明

“严格”或“急切”val 是未标记为延迟的 val。

在没有“早期定义”(见下文)的情况下,严格 val 的初始化按以下顺序进行。

  1. 超类在子类之前完全初始化。
  2. 否则,按声明顺序。

当然,当一个 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)

解决方案

避免空值的方法包括

使用惰性 val

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。如果您正在追求这个目标,您的选择包括

  1. 声明一个抽象严格 val,并希望子类将其实现为惰性 val 或早期定义。如果他们不这样做,它将在构造过程中某些时候显示为未初始化。
  2. 声明一个抽象 def,并希望子类将其实现为惰性 val。如果他们不这样做,它将在每次访问时重新评估。
  3. 声明一个会抛出异常的具体惰性 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 中指定。

请注意,早期定义在 Scala 2.13 中已弃用;它们将在 Scala 3 中被特质参数取代。因此,如果未来兼容性是一个问题,则不建议使用早期定义。

使用常量值定义

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 中指定。

此页面的贡献者