简体   繁体   中英

scala inheritance issue: val vs. def

Writing a simple example from Odersky's book resulted in the following problem:

// 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!

Things get even more weird when I replace val with def and evrth works perfect!

Could you please xplain this behaviour?

Here is a great article by Paul P that explains the initialization order intricacies in Scala. As a rule of thumb, you should never use abstract val s. Always use abstract def s and lazy val s.

In the definition of AbstractElement , you're in practice defining a constructor which initializes contents to null and computes contents.length. The constructor of UnifiedElement calls AbstractElement 's constructor and only then initializes contents.

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. Abstract vals are only one of the ways to trigger it.

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.

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:

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. 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.

Making contents a def here seems a bad idea, because it will be recomputed too often.

Finally, avoiding abstract vals is IMHO an extreme measure. Sometimes they are just the right thing - you should just be careful with concrete vals referring to abstract vals.

EDIT : It seems that instead of an abstract val, one could use an abstract definition and override it with a concrete val. That is indeed possible, but it does not help if there are concrete vals referring to the abstract definition. 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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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