Inheritance order in Scala

Posted by MattDunbar on Fri, 21 Jan 2022 20:20:43 +0100

1. Structural sequence

Give the following categories and characteristics as examples

trait AA {
  println("A...")
}

trait BB extends AA {
  println("B....")
}

trait CC extends BB {
  println("C....")
}

trait DD extends BB {
  println("D....")
}

class EE {
  println("E...")
}

class FF extends EE with CC with DD {
  println("F....")
}

class KK extends EE {
  println("K....")
}

1.1 declare classes and mix in characteristics at the same time

The construction order should be

① Call the superclass constructor of the current class
② The parent trait constructor of the first trait
③ First trait constructor
④ If the parent trait constructor of the second trait constructor has been executed, it will no longer be executed
⑤ The second trait constructor
⑥ Repeat steps 4 and 5 (if there is a third trait, the fourth trait)
⑦ Current class constructor

object TraitCreate {
  def main(args: Array[String]): Unit = {
    // Construct classes according to depth first
    // E -> A -> B -> C -> D -> F
    new FF
  }
}

1.2 when building objects, dynamically mix characteristics

The construction order should be

① Call the superclass constructor of the current class
② Current class constructor
③ Parent trait constructor of the first trait constructor
④ The first trait constructor
⑤ If the parent trait constructor of the second trait constructor has been executed, it will no longer be executed
⑥ The second trait constructor
⑦ Repeat steps 5 and 6 (if there are 3 and 4 traits)

object TraitCreate {
  def main(args: Array[String]): Unit = {
    // Construct the class according to the depth first principle
    // E -> K -> A -> B -> C -> D
    new KK with CC with DD
  }
}

1.3 summary

The difference between the construction order of the two ways of mixing traits is mainly reflected in the timing of the creation of the current class constructor

If the declared class is used to blend in the trait at the same time, the object has not been created when the trait is mixed in, so the current class constructor will not be created until the trait constructor is created.
If you dynamically mix in traits when building an object, it can be understood that when you mix in traits, the object has been created. Therefore, when you call the superclass constructor without mixing traits, the current class constructor has been created.

2. Order of calling super method

2.1 linearization in Scala

The method called by super in the trait depends on the * * * linearization * * * of the class and the traits mixed into the class.

When you instantiate a class with new, Scala will take out the class and all its inherited classes and characteristics and arrange them linearly. Then, when a super is called in a class, the called method is the last one in the chain (the one on the far right). If all methods except the last one call super, the final result is the superimposed behavior.

The exact order of linearization is described in Scala Language Specification. This description is a little complicated. It is explained in detail in the following website. Interested students can go on to study it:

Scala Language Specification - Classes and Objects

In general, that is, in any linearization, a class always precedes all its superclasses and mixed qualities.

but the main thing you need to know is that, in any linearization, a class is always linearized in front of all its superclasses and mixed in traits.

Therefore, when you write down a method that calls super, that method is definitely modifying the behavior of superclasses and mixing traits, not vice versa. It's a little around here. My understanding is to call the super method in the opposite direction of the constructor until super calls the method with the same name of the object at the deepest layer

Take the case in Programming in Scala written by Martin Odersky as an example

object Linearization {
  def main(args: Array[String]): Unit = {
    /**
     * Output results
     *    Animal
     *    Furry
     *    HasLegs
     *    FourLegged
     *    Cat
     *    Animal:Furry:HasLegs:FourLegged:Cat:miao miao~
     */
    val cat = new Cat
    cat.printInfo("miao miao~")
  }
}

class Animal {
  println("Animal")
  def printInfo(msg: String): Unit = println(s"Animal:${msg}")
}

trait Furry extends Animal {
  println("Furry")
  override def printInfo(msg: String): Unit = super.printInfo(s"Furry:${msg}")
}

trait HasLegs extends Animal {
  println("HasLegs")
  override def printInfo(msg: String): Unit = super.printInfo(s"HasLegs:${msg}")
}

trait FourLegged extends HasLegs {
  println("FourLegged")
  override def printInfo(msg: String): Unit = super.printInfo(s"FourLegged:${msg}")
}

class Cat extends Animal with Furry with FourLegged {
  println("Cat")
  override def printInfo(msg: String): Unit = super.printInfo(s"Cat:${msg}")
}

The inheritance relationship and linearization of Cat classes are shown in the following figure

Cat linearization is calculated from back to front as follows. The last part of cat linearization is the linearization of its super class Animal. This linearization is copied directly without modification. Since Animal does not explicitly extend a super class and does not mix Any super characteristics, it extends from AnyRef by default, and AnyRef extends from Any. Thus, the linearization of Animal looks like this:

Animal --> AnyRef --> Any

The penultimate part of linearization is the linearization of the first blending (i.e. the Furry trait), but all classes that have appeared in Animal linearization will not appear again, and each class only appears once in Cat linearization. The result is:

Furry --> Animal --> AnyRef --> Any

Before this result, it is the linearization of FourLegged. Similarly, any class that has been copied in the superclass or the first blend will not appear again:

FourLeggerd --> HasLegs -- >Furry --> Animal --> AnyRef --> Any

Finally, the first class in Cat linearization:

Cat --> FourLeggerd --> HasLegs -- >Furry --> Animal --> AnyRef --> Any

The linearization process of Cat class is shown in the following table

2.2 case description

According to the above ideas, the following cases are sorted out

trait foo06 {
  println("foo06")
  def foo(msg: String): Unit = {
    println("I'm in foo06")
    println(s"foo06:${msg}")
  }
}

trait foo07 {
  println("foo07")
  def foo(msg: String): Unit = {
    println("I'm in foo07")
    println(s"foo07:${msg}")
  }
}

trait foo08 {
  println("foo08")
  def foo(msg: String): Unit = {
    println("I'm in foo08")
    println(s"foo08:${msg}")
  }
}

trait foo09 {
  println("foo09")
  def foo(msg: String): Unit = {
    println("I'm in foo09")
    println(s"foo09:${msg}")
  }
}

trait foo04 extends foo08 with foo06 with foo07 {
  println("foo04")
  override def foo(msg: String): Unit = {
    println("I'm in foo04")
    super.foo(s"foo04:${msg}")
  }
}

trait foo05 extends foo08 with foo09 {
  println("foo05")
  override def foo(msg: String): Unit = {
    println("I'm in foo05")
    super.foo(s"foo05:${msg}")
  }
}

trait foo01 extends foo04 with foo05 {
  println("foo01")
  override def foo(msg: String): Unit = {
    println("I'm in foo01")
    super.foo(s"foo01:${msg}")
  }
}

trait foo02 extends foo04 {
  println("foo02")
  override def foo(msg: String): Unit = {
    println("I'm in foo02")
    super.foo(s"foo02:${msg}")
  }
}

trait foo03 extends foo05 with foo04 with foo02 with foo07 {
  println("foo03")
  override def foo(msg: String): Unit = {
    println("I'm in foo03")
    super.foo(s"foo03:${msg}")
  }
}

The inheritance relationship is shown in the figure below. Numbers represent the inheritance order. The larger the number, the more right it is. Red represents the inheritance relationship closest to the right.

Create the following classes and test them

object DiamondInherited {

  def main(args: Array[String]): Unit = {
    val fooApp = new FooApp
    fooApp.foo("fooApp")
  }

}
// foo01 --> foo02 --> foo03
class FooApp extends foo01 with foo02 with foo03 {
  override def foo(msg: String): Unit = super.foo(msg)
}

give the result as follows

# Construct according to the principle of depth first
foo08
foo06
foo07
foo04
foo09
foo05
foo01
foo02
foo03
# Linearize method call order
I'm in foo03
I'm in foo02
I'm in foo01
I'm in foo05
I'm in foo09
# Final output
foo09:foo05:foo01:foo02:foo03:fooApp

It can be seen that when calling with super, the method on the far right will be called, and the reconstructed method will be called in the opposite direction of construction until it is called to the lowest method.

Now adjust the inheritance order in FooApp

object DiamondInherited {

  def main(args: Array[String]): Unit = {
    val fooApp = new FooApp
    fooApp.foo("fooApp")
  }

}
// foo03 --> foo02 --> foo01
class FooApp extends foo01 with foo02 with foo03 {
  override def foo(msg: String): Unit = super.foo(msg)
}

The results are as follows, in line with expectations

# Construct according to the principle of depth first
foo08
foo09
foo05
foo06
foo07
foo04
foo02
foo03
foo01
# Linearize method call order
I'm in foo01
I'm in foo03
I'm in foo02
I'm in foo04
I'm in foo07
# Final output
foo07:foo04:foo02:foo03:foo01:fooApp

Of course, if a trait in the inheritance process does not override the method or call the super method, the super calling class will end when it reaches the trait

object DiamondInherited {

  def main(args: Array[String]): Unit = {
    val fooApp = new FooApp
    fooApp.foo("fooApp")
  }

}

// Comment out the super call in foo02, and other characteristics remain unchanged
trait foo02 extends foo04 {
  println("foo02")
  override def foo(msg: String): Unit = {
    println("I'm in foo02")
    //super.foo(s"foo02:${msg}")
  }
}

class FooApp extends foo03 with foo02 with foo01 {
  override def foo(msg: String): Unit = super.foo(msg)
}

The output results are as follows

# Construct according to the principle of depth first
foo08
foo09
foo05
foo06
foo07
foo04
foo02
foo03
foo01
# Linearize method call order
I'm in foo01
I'm in foo03
I'm in foo02
# Since the super method is not called in foo2, the call chain has ended so far, and the result cannot be output

2.3 specify the characteristics to be called

The above examples are all methods that determine the characteristics to be called through the integration relationship. If you need to specify which characteristics to use, you can directly specify them through super[clazz]

object TraitOverlying {
  def main(args: Array[String]): Unit = {
    val myFootBall = new MyFootBall
    // my ball is a foot-ball
    println(myFootBall.describe())
  }
}

// Defining ball characteristics
trait Ball {
  def describe(): String = "ball"
}

// Define color characteristics
trait ColorBall extends Ball {
  var color: String = "red"
  override def describe(): String = color + "-" + super.describe()
}

// Define category characteristics
trait CategoryBall extends Ball {
  var category: String = "foot"
  override def describe(): String = category + "-" + super.describe()
}

// Define a custom ball class
class MyFootBall extends CategoryBall with ColorBall {
  // Specify the method of the parent class to be called through super[clazz]
  override def describe(): String = "my ball is a " + super[CategoryBall].describe()
}

2.4 summary

  • The method called by super in the trait depends on the linearization of the class and the traits mixed into the class
  • When calling super in a class, the called method is the last one up to the top of the chain (the one on the right), and if the parent method still has super method, the chain call will be called.
  • The super method is called in the reverse order of the constructor until the method in the lowest parent class is called
  • You can specify the specific parent class to be called by super[clazz]

Topics: Scala