首先,让我们看看我们用特质(Trait)能做到的最简单的事情: 我们是如何处理一个混入了多个特质的类型,就好像它实现了这些"带有实现的接口" - 如果你来自 Java 世界,你有可能会试图这样称呼特质。
class Base { def b = "" }
trait Cool { def c = "" }
trait Awesome { def a ="" }
class BA extends Base with Awesome
class BC extends Base with Cool
// 正如你所预料的,你可以将这些实例转换成它们混入的任意特质类型
val ba: BA = new BA
val bc: Base with Cool = new BC
val b1: Base = ba
val b2: Base = bc
ba.a
bc.c
b1.b
到目前为止,这对你来说应该是相当直接的。现在让我们深入"菱形问题"的世界,C++ 开发人员应该熟悉这一点。 基本上,"菱形问题"就是存在多重继承的情况下,我们无法确定哪个是直接父类。 如果我们将特质组合看成是多重继承的用法,那么下面的图片就演示了这个问题:
为了产生"菱形问题",我们只要在 B
或者/并且 C
中有一个覆盖实现。
这样的话,当调用 D 的方法时,我们就引入了歧义性。在 D 中,我们调用的是继承来自 C 还是来自 B 的方法?
在 Scala 中,只有一个覆盖方法时是非常简单的 - 覆盖方法胜利。但是,让我们考虑更复杂的案例:
-
类
A
定义了一个方法common
返回a
, -
特质
B
覆盖了common
返回b
, -
特质
C
覆盖了common
返回c
, -
类
D
继承了B
andC
, -
类
D
继承了哪个版本的common
方法呢?是来自C
的覆盖实现,还是来自B
的呢?
这种模糊性是每个类似多继承机制的痛点。Scala 通过所谓的 Type Linearization 解决这个问题。
换句话说,给定一个菱形的类结构,我们总是(确定性地)可以决定在 D 的内部调用 common
时哪个覆盖方法会被调用。
让我们用代码实现,然后再讨论线性化:
trait A { def common = "A" }
trait B extends A { override def common = "B" }
trait C extends A { override def common = "C" }
class D1 extends B with C
class D2 extends C with B
检查上述类型,我们获得下面的运行时行为:
(new D1).common == "C"
(new D2).common == "B"
之所以会出现这样的结果,是因为在这里 Scala 为我们应用了 type linearization。算法流程如下:
-
从头构建一个类型的列表,列表的第一个元素是我们正在线性化的类型
-
递归地扩展每个父类,并把这些类型都放到这个列表中(列表应该是平坦而不是嵌套的)
-
从结果列表中删除重复项,从列表左边开始扫描,去除已经"看到过"的类型
-
完成
让我们对上面的菱形例子应用这个算法,验证为什么 D1 extends B with C
(和 D2 extends C with B
)会返回这样的结果:
// 从 D1 开始
B with C with <D1>
// 对每一个类型,扩展它直到到达 Any
(Any with AnyRef with A with B) with (Any with AnyRef with A with C) with <D1>
// 从左到右,通过删除"已经看到"的类型,去除冗余
(Any with AnyRef with A with B) with ( C) with <D1>
// 书写最后的结果类型
Any with AnyRef with A with B with C with <D1>
现在调用 common
方法时,我们可以很简单的决定调用的是哪个版本:
我们只需要查看线性化类型,并尝试从右向左解析方法调用。
在 D1
的例子中,处于"最右边"并且能提供的 common
实现的是特质 C,
所以它覆盖了 B
提供的 common
实现。那么在 D1
内部调用 common
的结果就是 "c"
。
你可以通过对类 D2
运行这个算法来加深理解 - B
应该线性化在 C
的右边,所以当你运行代码时会返回一个 "b"
。
对于这样简单的线性化例子,我们可以仅考虑"最右边的赢",这样的想法非常简单易于理解,但是没有给出线性化算法的全貌。
值得一提的是,使用这个技巧,我们现在还可以回答*"谁是我的 super
?*。对于任意的类,如果你想检查谁是你的父类,只需要检查线性化类型的左边。比如在我们的例子(D1
)中,C
的父类是 B
。