简体   繁体   English

scala继承问题:val与def

[英]scala inheritance issue: val vs. def

Writing a simple example from Odersky's book resulted in the following problem: 从Odersky的书中写一个简单的例子导致了以下问题:

// AbstractElement.scala
abstract class AbstractElement {
  val contents: Array[String]
  val height: Int = contents.length // line 3
}

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { // line 6
  val contents = Array.fill(_height)(ch.toString() * _width)
}

object AbstractElement {
  def create(ch: Char): AbstractElement = {
    new UnifiedElement(ch, 1, 1) // line 12
  }
}

,

// ElementApp.scala
import AbstractElement.create

object ElementApp {

  def main(args: Array[String]): Unit = {
  val e1 = create(' ') // line 6
    println(e1.height)
  }
}

The compiler throws the following trace: 编译器抛出以下跟踪:

Exception in thread "main" java.lang.NullPointerException
    at AbstractElement.<init>(AbstractElement.scala:3)
    at UnifiedElement.<init>(AbstractElement.scala:6)
    at AbstractElement$.create(AbstractElement.scala:12)
    at ElementApp$.main(ElementApp.scala:6)
    at ElementApp.main(ElementApp.scala)

So the compiler thinks that contents is still null, but I defined it in UnifiedContainer! 所以编译器认为内容仍为null,但我在UnifiedContainer中定义了它!

Things get even more weird when I replace val with def and evrth works perfect! 当我用def和evrth替换val完美时,事情变得更加奇怪!

Could you please xplain this behaviour? 你能否解释一下这种行为?

Here is a great article by Paul P that explains the initialization order intricacies in Scala. 是Paul P的一篇很棒的文章,它解释了Scala中的初始化顺序错综复杂。 As a rule of thumb, you should never use abstract val s. 根据经验,你绝不应该使用抽象的val Always use abstract def s and lazy val s. 始终使用抽象def S和lazy val秒。

In the definition of AbstractElement , you're in practice defining a constructor which initializes contents to null and computes contents.length. AbstractElement的定义中,您实际上正在定义一个构造函数,该构造函数将内容初始化为null并计算contents.length。 The constructor of UnifiedElement calls AbstractElement 's constructor and only then initializes contents. UnifiedElement的构造UnifiedElement调用AbstractElement的构造函数,然后才初始化内容。

EDIT : in other words, we have a new instance of a problem already existing in Java (and any OOP language): the constructor of a superclass calls a method implemented in a subclass, but the latter cannot be safely called because the subclass is not yet constructed. 编辑 :换句话说,我们有一个已经存在于Java(和任何OOP语言)中的问题的新实例:超类的构造函数调用在子类中实现的方法,但后者不能安全地调用,因为子类不是尚未构建。 Abstract vals are only one of the ways to trigger it. 抽象vals只是触发它的方法之一。

The simplest solution here is to just make height a def , which is better anwyay, and be aware of initialization rules linked in the other answer. 这里最简单的解决方案是将height设为def ,这是更好的anwyay,并且要注意在另一个答案中链接的初始化规则。

abstract class AbstractElement {
  val contents: Array[String]
  def height: Int = contents.length //Make this a def
}

The slightly more complex solution, instead, is to force contents to be initialized before height, which you can do with this syntax: 相反,稍微复杂的解决方案是强制contents 高度之前初始化,您可以使用以下语法执行此操作:

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends {
  val contents = Array.fill(_height)(ch.toString() * _width)
} with AbstractElement {
  //...
}

Note that mixin composition, that is with , is not symmetrical - it works left-to-right. 请注意,mixin组合, with对称,不是对称的 - 它从左到右工作。 And note that {} at the end can be omitted, if you define no other members. 请注意,如果您没有定义其他成员,则可以省略最后的{}

Lazy vals are also a solution, but they incur quite some run-time overhead - whenever you read the variable, the generated code will read a volatile bitmap to check that the field was already initialized. 延迟val也是一种解决方案,但它们会产生相当多的运行时开销 - 每当您读取变量时,生成的代码将读取易失性位图以检查该字段是否已初始化。

Making contents a def here seems a bad idea, because it will be recomputed too often. 在这里制作contents def似乎是一个坏主意,因为它将被重新计算得太频繁。

Finally, avoiding abstract vals is IMHO an extreme measure. 最后,避免抽象的val是恕我直言,这是一个极端的措施。 Sometimes they are just the right thing - you should just be careful with concrete vals referring to abstract vals. 有时它们是正确的 - 你应该小心使用抽象vals的具体val。

EDIT : It seems that instead of an abstract val, one could use an abstract definition and override it with a concrete val. 编辑 :似乎不是抽象的val,而是可以使用抽象定义并用具体的val覆盖它。 That is indeed possible, but it does not help if there are concrete vals referring to the abstract definition. 这确实是可能的,但如果有具体的val引用抽象定义则没有用。 Consider this variant of the above code, and pay attention to how members are defined: 考虑上面代码的这个变体,并注意如何定义成员:

abstract class AbstractElement {
  def contents: Array[String]
  val height: Int = contents.length // line 3
}

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement {
  val contents = Array.fill(_height)(ch.toString() * _width)
}

This code has the same runtime behavior as the code given by the OP, even if AbstractElement.contents is now a def : the body of the accessor reads a field which is initialized only by the subclass constructor. 此代码具有与OP给出的代码相同的运行时行为,即使AbstractElement.contents现在是def :访问者的主体读取仅由子类构造函数初始化的字段。 The only difference between an abstract value and an abstract definition seems to be that an abstract value can only be overridden by a concrete value, so it can be useful to constrain the behavior of subclasses if that is what you want. 抽象值和抽象定义之间的唯一区别似乎是抽象值只能被具体值覆盖,因此如果这是您想要的,那么约束子类的行为会很有用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM