根据特征中的抽象def获取val的null

时间:2015-04-16 11:46:33

标签: scala

在我的特质中混合使用val和def时,我看到了一些初始化的怪异。可以通过以下示例总结这种情况。

我有一个提供抽象字段的特性,我们称之为fruit,它应该在子类中实现。它还在val中使用该字段:

scala> class FruitTreeDescriptor(fruit: String) {
     |   def describe = s"This tree has loads of ${fruit}s"
     | }
defined class FruitTreeDescriptor

scala> trait FruitTree {
     |   def fruit: String
     |   val descriptor = new FruitTreeDescriptor(fruit)
     | }
defined trait FruitTree

当使用fruit覆盖def时,事情会按预期进行:

scala> object AppleTree extends FruitTree {
     |   def fruit = "apple"
     | }
defined object AppleTree

scala> AppleTree.descriptor.describe
res1: String = This tree has loads of apples

但是,如果我使用fruit ...

覆盖val
scala> object BananaTree extends FruitTree {
     |   val fruit = "banana"
     | }
defined object BananaTree

scala> BananaTree.descriptor.describe
res2: String = This tree has loads of nulls

这里发生了什么?

3 个答案:

答案 0 :(得分:4)

简单来说,就在你打电话的时候:

val descriptor = new FruitTreeDescriptor(fruit)

BananaTree的构造函数尚未有机会运行。这意味着fruit的值仍为null,即使它是val

这是[{1}} 的非声明性初始化的众所周知的怪癖的子案例,可以用更简单的例子来说明:

vals

(幸运的是,在这种特殊情况下,编译器会检测正在发生的事情,并会发出警告。)

要避免此问题,请将class A { val x = a val a = "String" } scala> new A().x res1: String = null 声明为fruit,这会强制进行评估。

答案 1 :(得分:2)

问题是初始化顺序。 val fruit = ...之后正在初始化val descriptor = ...,因此在初始化descriptor时,fruit仍然是null。您可以通过fruit lazy val来解决此问题,因为它会在首次访问时初始化。

答案 2 :(得分:1)

您的descriptor字段初始化早于fruit字段,因为特征早于类的初始化,扩展了它。 null是初始化之前的字段值 - 这就是您获取它的原因。在def情况下,它只是一个方法调用而不是访问某个字段,所以一切都很好(因为方法的代码可能被调用多次 - 这里没有初始化)。请参阅http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html

为什么def如此不同?这是因为def可能被调用多次,但是val - 只调用一次(所以它的第一个也是唯一一个调用实际上是fileld的初始化)。

此类问题的典型解决方案 - 使用lazy val代替它,它会在您真正需要时初始化。另一个解决方案是early intializers

另一个更简单的例子:

scala> class A {val a = b; val b = 5}
<console>:7: warning: Reference to uninitialized value b
       class A {val a = b; val b = 5}
                        ^
defined class A

scala> (new A).a
res2: Int = 0 //null

更一般地说,理论上scala可以分析字段之间的依赖图(哪个字段需要其他字段)并从最终节点开始初始化。但实际上每个模块都是单独编译的,编译器可能甚至不知道那些依赖项(它甚至可能是调用Scala的Java调用Java),所以它只是进行顺序初始化。

因此,因此,它甚至无法检测到简单的循环:

scala> class A {val a: Int = b; val b: Int = a}
<console>:7: warning: Reference to uninitialized value b
       class A {val a: Int = b; val b: Int = a}
                             ^
defined class A

scala> (new A).a
res4: Int = 0

scala> class A {lazy val a: Int = b; lazy val b: Int = a}
defined class A

scala> (new A).a
java.lang.StackOverflowError

实际上,这样的循环(在一个模块内部)理论上可以在单独的构建中检测到,但它不会有太多帮助,因为它非常明显。