Scala 的 trait mix-in 调用链

Scala's trait mix-in call chain

我有这个 Scala 代码:

trait Foo {
    def foo()
}

trait M extends Foo {
    abstract override def foo() {println("M"); super.foo()}
}

// interface implementation
class FooImpl1 extends Foo {
    override def foo() {println("Impl")}
}

class FooImpl2 extends FooImpl1 with M

object Main extends App {
    val a = new FooImpl2
    a.foo 
}

执行时打印出

M
Impl

我很好奇的是特征方法背后的机制。在这种情况下,首先调用特征 M 中的 foo,然后 super.foo() 调用 FooImpl1.foo() 的具体调用。这个调用链背后的逻辑是什么?是否有关于此行为的文档?

是的,它被称为 "linearization" and it solves the hated Diamond Problem of Multiple Inherithance 非常聪明。

检查链接(或 original paper,你会学到比我能给出的任何快速答案都多的东西,但基本思想是这样的:你从多个特征或抽象继承的顺序 class很重要。Scala 将通过按顺序选择父代并调用最接近的可用覆盖来创建一个没有中断的单一继承线。

或者更好的是,查看 Chapter 12 of Programming in Scala, First Edition 中的规范示例:


以下示例说明了 Scala 线性化的主要属性:假设您有一只 class Cat,它继承自超级 class Animal 和两个特征 Furry 和 FourLegged。 FourLegged 又扩展了另一个 trait HasLegs:

 class Animal 
  trait Furry extends Animal
  trait HasLegs extends Animal
  trait FourLegged extends HasLegs
  class Cat extends Animal with Furry with FourLegged

ClassCat的继承层次和线性化如图12.1所示。使用传统的 UML 符号表示继承:3 带有白色三角形箭头的箭头表示继承,箭头指向超类型。带有深色非三角形箭头的箭头表示线性化。深色箭头指向超级调用将被解析的方向。

图 12.1 - class 类别的继承层次和线性化

Cat 的线性化从后向前计算如下。 Cat 线性化的最后一部分是它的 superclass, Animal 的线性化。这种线性化被复制而没有任何变化。 (每个类型的线性化显示在 Table 12.1 此处。)因为 Animal 没有显式扩展 superclass 或混合任何超特征,它默认扩展 AnyRef,后者扩展 Any。因此,动物的线性化看起来像:

倒数第二部分是第一个 mixin 特征 Furry 的线性化,但是现在所有已经在 Animal 线性化中的 classes 都被排除在外,因此每个 class在 Cat 的线性化中只出现一次。结果是:

这之前是 FourLegged 的​​线性化,其中任何已经在 superclass 或第一个 mixin 的线性化中被复制的 classes 再次被排除在外:

最后,Cat的线性化中第一个class就是Cat本身:

当这些 classes 和特征中的任何一个通过 super 调用方法时,调用的实现将是线性化中其右侧的第一个实现。