在我的特质中混合使用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
这里发生了什么?
答案 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
实际上,这样的循环(在一个模块内部)理论上可以在单独的构建中检测到,但它不会有太多帮助,因为它非常明显。