4

今天在处理更多面向对象的部分时,我得到了一些 NPE——我不知道为什么,但是在构造函数中存在用于覆盖字段的空值。

有什么办法可以解决这个问题吗?这是一个演示该行为的示例。您会注意到基类的 println 会导致打印 null。如果基类试图在基类的构造函数中对“line”做任何事情,它将触发一个 npe。

scala> class Base {val line = "hey1"; println(line)}
defined class Base

scala> class Extended extends Base{ override val line = "hey2"; println(line)}
defined class Extended

scala> new Extended
null
hey2
res0: Extended = Extended@55991e21

scala> new Base
hey1
res1: Base = Base@1cc21a68

例如,这里是一个演示空指针异常的示例。

scala> class Base { val line = "hello"; println(line.reverse)}
defined class Base
         ^
scala> class Extend extends Base { override val line ="exthello"; println(line.reverse);}
defined class Extend

scala> new Extend
java.lang.NullPointerException
4

2 回答 2

3

如果你想保持你的结构,而不是使用 def 或lazy val 而不是 val:

class Base {def line = "hey1"; println(line)}
class Extend extends Base { override def line ="exthello"; println(line);}

但最好在参数列表中覆盖:

scala> class Base(val a: String = "hey1") {println(a)}
defined class Base

scala> new Base
hey1

scala> class Extend(override val a:String = "hey2") extends Base(a) {println(a)}

defined class Extend

scala> new Extend
hey2
hey2
res18: Extend = Extend@4d7efd4
于 2013-09-03T18:44:26.923 回答
3

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

于 2013-09-04T19:13:53.470 回答