Scala的特质混入调用链

7

我有这段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()的具体调用。这个调用链背后的逻辑是什么?是否有关于这种行为的文档?

1个回答

12

是的,这被称为"线性化",它以一种非常聪明的方式解决了多重继承中令人讨厌的菱形问题

查看链接(或原始论文),你将比我能给出的任何快速答案更多地了解到这个问题,但基本思想是:从多个特征或抽象类继承的顺序很重要。Scala将创建一个单一的继承线,没有断层,通过按顺序选择父级并调用最接近的覆盖。

或者更好的是,查看《Scala编程》第一版第12章的经典示例


Scala的线性化主要特性可以通过以下示例进行说明:假设您有一个名为Cat的类,它继承自一个超类Animal和两个trait 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

Class Cat的继承层次结构和线性化如图12.1所示。继承使用传统的UML符号表示: 3 白色三角箭头的箭头表示继承,箭头指向超类型。黑色非三角箭头的箭头描绘了线性化。黑色箭头指向超级调用将被解决的方向。

image images/linearization.jpg

图12.1 - 类Cat的继承层次和线性化。

Cat的线性化从后往前计算,最后一部分是其父类Animal的线性化。这个线性化被复制并保持不变。(这些类型的线性化在此处的表12.1中显示。) 因为Animal没有显式地扩展任何超类或混合任何超特征,所以默认情况下它扩展了AnyRef,而AnyRef又扩展了Any。因此,Animal的线性化看起来像:

image images/AnimalLine.jpg

倒数第二部分是对第一个mixin,即Furry特质的线性化,但所有已经在Animal线性化中的类现在都被排除在外,这样每个类在Cat的线性化中只出现一次。结果如下:

image images/FurryLine.jpg

这是在 FourLegged 的线性化之前进行的,其中任何已经在超类或第一个 mixin 的线性化中被复制的类都会被省略:

image images/FourLeggedLine.jpg

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

image images/CatLine.jpg

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

很好的答案!一个问题 - 如果我从 class Cat extends Animal with Furry with FourLegged 改为 class Cat extends Animal with FourLegged with Furry,这会改变什么吗? - TheMP
1
你可以按照相同的步骤,逐步添加类。这样,你会得到 cat --> Furry --> FourLegged --> HasLegs --> Animal --> AnyRef --> Any。注意顺序被反转了吗?在决定重写方法调用时,这很重要。特别是,请阅读第12章的“可堆叠修改”部分。 - Daniel Langdon
第一次实现在它的左边,肯定是吧? - itsbruce
根据问题中的解释和示例,我相信我做对了... - Daniel Langdon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接