目录

Scala-中trait的线性化规则Linearization-Rule和-super-的调用行为

Scala 中trait的线性化规则(Linearization Rule)和 super 的调用行为

在 Scala 中, 特质(Trait) 是一种强大的工具,用于实现代码的复用和组合。当一个类混入( with )多个特质时,可能会出现方法冲突的情况。为了解决这种冲突,Scala 引入了 最右优先原则(Rightmost First Rule) ,也称为 线性化规则(Linearization Rule)

最右优先原则

最右优先原则的核心思想是: 在混入多个特质时,最右边的特质会优先生效 。也就是说,如果一个方法在多个特质中都有定义,那么最右边的特质中的方法会覆盖左边特质中的方法。

示例1
trait A {
  def greet(): String = "Hello from A"
}

trait B {
  def greet(): String = "Hello from B"
}

class C extends A with B {
  override def greet(): String = super.greet()
}

val obj = new C
println(obj.greet())  // 输出: Hello from B

在上面的例子中:

  • C 混入了特质 AB
  • 根据最右优先原则, B 中的 greet 方法会覆盖 A 中的 greet 方法。
  • 因此,调用 obj.greet() 时,输出的是 B 中的实现。

线性化规则

最右优先原则是 Scala 线性化规则的一部分。Scala 会为每个类生成一个 线性化顺序(Linearization Order) ,这个顺序决定了方法调用的优先级。

线性化顺序的生成规则
  1. 类的线性化顺序从最具体的类开始,逐步向更通用的类扩展。
  2. 混入的特质按照从右到左的顺序排列。
  3. 每个特质只会在线性化顺序中出现一次。
示例2
trait A {
  def greet(): String = "Hello from A"
}

trait B extends A {
  override def greet(): String = "Hello from B"
}

trait C extends A {
  override def greet(): String = "Hello from C"
}

class D extends B with C {
  override def greet(): String = super.greet()
}

val obj = new D
println(obj.greet())  // 输出: Hello from C

在这个例子中:

  • D 的线性化顺序是: D -> C -> B -> A
  • 根据最右优先原则, C 中的 greet 方法会覆盖 B 中的 greet 方法。
  • 因此,调用 obj.greet() 时,输出的是 C 中的实现。

super 的调用

在特质中, super 的调用是动态绑定的,它会根据线性化顺序调用下一个特质或类中的方法。

示例3
trait A {
  def greet(): String = "Hello from A"
}

trait B extends A {
  override def greet(): String = s"${super.greet()} and Hello from B"
}

trait C extends A {
  override def greet(): String = s"${super.greet()} and Hello from C"
}

class D extends B with C {
  override def greet(): String = super.greet()
}

val obj = new D
println(obj.greet())  // 输出: Hello from A and Hello from B and Hello from C

如果你还是有疑问,接下来,是更加具体的分析:

在示例3中,输出的是Hello from A and Hello from B and Hello from C ,而不是 Hello from A and Hello from C and Hello from B 。这看起来似乎与最右优先原则相矛盾,但实际上这是由 Scala 的线性化规则(Linearization Rule)决定的。

线性化规则详解

Scala 的线性化规则决定了方法调用的顺序。具体来说,当一个类混入多个特质时,Scala 会生成一个 线性化顺序 ,这个顺序决定了 super 调用的行为。

线性化顺序的生成规则
  1. 从最具体的类开始 ,逐步向更通用的类扩展。
  2. 混入的特质按照从右到左的顺序排列
  3. 每个特质只会在线性化顺序中出现一次

在示例3中:

class D extends B with C
  • D 的线性化顺序是: D -> C -> B -> A
线性化顺序的解释
  1. D :最具体的类。
  2. C :因为 C 是最右边的特质,所以它排在 B 前面。
  3. BB 是左边的特质,排在 C 后面。
  4. AABC 的共同父特质,排在最后。

因此, D 的线性化顺序是: D -> C -> B -> A

super 的调用行为

在 Scala 中, super 的调用是动态绑定的,它会根据线性化顺序调用下一个特质或类中的方法。

例子分析
trait A {
  def greet(): String = "Hello from A"
}

trait B extends A {
  override def greet(): String = s"${super.greet()} and Hello from B"
}

trait C extends A {
  override def greet(): String = s"${super.greet()} and Hello from C"
}

class D extends B with C {
  override def greet(): String = super.greet()
}

val obj = new D
println(obj.greet())  // 输出: Hello from A and Hello from B and Hello from C
  1. D 中的 greet 方法

    • 调用 super.greet() ,根据线性化顺序, super 指向 C
  2. C 中的 greet 方法

    • 调用 super.greet() ,根据线性化顺序, super 指向 B
  3. B 中的 greet 方法

    • 调用 super.greet() ,根据线性化顺序, super 指向 A
  4. A 中的 greet 方法

    • 返回 "Hello from A"
  5. 方法调用的堆栈

    • A 返回 "Hello from A"
    • B 在其基础上追加 " and Hello from B" ,得到 "Hello from A and Hello from B"
    • C 在其基础上追加 " and Hello from C" ,得到 "Hello from A and Hello from B and Hello from C"

为什么不是 Hello from A and Hello from C and Hello from B

  • 因为 super 的调用是根据线性化顺序动态绑定的,而不是简单地按照最右优先原则直接覆盖。
  • 线性化顺序是 D -> C -> B -> A ,所以 Csuper 指向 BBsuper 指向 A
  • 因此, Cgreet 方法会先调用 Bgreet 方法,而 Bgreet 方法会调用 Agreet 方法。

总结

  • 最右优先原则 :决定了特质的优先级,最右边的特质会优先生效。
  • 线性化规则 :决定了 super 的调用顺序, super 会根据线性化顺序动态绑定到下一个特质或类。
  • 在示例3中,线性化顺序是 D -> C -> B -> A ,因此输出的顺序是 Hello from A and Hello from B and Hello from C

在示例2中,为什么输出是 Hello from C ,而不是 Hello from A and Hello from C?


代码分析

trait A {
  def greet(): String = "Hello from A"
}

trait B extends A {
  override def greet(): String = "Hello from B"
}

trait C extends A {
  override def greet(): String = "Hello from C"
}

class D extends B with C {
  override def greet(): String = super.greet()
}

val obj = new D
println(obj.greet())  // 输出: Hello from C
  1. 特质的继承关系

    • BC 都继承自 A ,并且都重写了 greet 方法。
    • D 混入了 BC ,并且重写了 greet 方法,调用了 super.greet()
  2. 线性化顺序

    • D 混入 BC 时,Scala 会生成一个线性化顺序。线性化顺序的规则是:

      • 从最具体的类开始,逐步向更通用的类扩展。
      • 混入的特质按照从右到左的顺序排列。
      • 每个特质只会在线性化顺序中出现一次。
    • 对于 class D extends B with C ,线性化顺序是: D -> C -> B -> A

  3. super 的调用行为

    • Dgreet 方法中, super.greet() 会根据线性化顺序调用下一个特质或类中的 greet 方法。
    • 线性化顺序是 D -> C -> B -> A ,因此 super.greet() 会调用 C 中的 greet 方法。
  4. C 中的 greet 方法

    • C 中的 greet 方法直接返回 "Hello from C"没有调用 super.greet()
    • 因此, Cgreet 方法不会继续调用 BAgreet 方法。

为什么输出是 Hello from C

  • Dgreet 方法中, super.greet() 调用的是 Cgreet 方法。
  • Cgreet 方法直接返回 "Hello from C" ,没有继续调用 super.greet() (即没有调用 BAgreet 方法)。
  • 因此,最终的输出是 "Hello from C"

为什么不是 Hello from A and Hello from C

  • 如果希望输出 Hello from A and Hello from C需要在 Cgreet 方法中显式调用 super.greet() ,将 A 的行为与 C 的行为组合起来。
  • 例如:
trait C extends A {
  override def greet(): String = s"${super.greet()} and Hello from C"
}

修改后, Cgreet 方法会先调用 Agreet 方法,然后追加 " and Hello from C" 。此时,输出会是 Hello from A and Hello from C


修改后的代码

trait A {
  def greet(): String = "Hello from A"
}

trait B extends A {
  override def greet(): String = "Hello from B"
}

trait C extends A {
  override def greet(): String = s"${super.greet()} and Hello from C"
}

class D extends B with C {
  override def greet(): String = super.greet()
}

val obj = new D
println(obj.greet())  // 输出: Hello from A and Hello from C

总结

  • 默认行为 :在 Cgreet 方法中,如果没有调用 super.greet() ,则只会执行 C 的逻辑,输出 Hello from C
  • 组合行为 :如果希望将父特质的行为与当前特质的行为组合起来,需要在重写方法时显式调用 super.greet()
  • 线性化顺序super 的调用是根据线性化顺序动态绑定的,线性化顺序决定了方法调用的优先级。