Skip to content

Latest commit

 

History

History
100 lines (69 loc) · 4.39 KB

traits.adoc

File metadata and controls

100 lines (69 loc) · 4.39 KB

Traits, as in "interfaces with implementation"

首先,让我们看看我们用特质(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++ 开发人员应该熟悉这一点。 基本上,"菱形问题"就是存在多重继承的情况下,我们无法确定哪个是直接父类。 如果我们将特质组合看成是多重继承的用法,那么下面的图片就演示了这个问题:

Type Linearization vs. The Diamond Problem

Diamond Inheritance

为了产生"菱形问题",我们只要在 B 或者/并且 C 中有一个覆盖实现。 这样的话,当调用 D 的方法时,我们就引入了歧义性。在 D 中,我们调用的是继承来自 C 还是来自 B 的方法? 在 Scala 中,只有一个覆盖方法时是非常简单的 - 覆盖方法胜利。但是,让我们考虑更复杂的案例:

  • A 定义了一个方法 common 返回 a,

  • 特质 B 覆盖了 common 返回 b,

  • 特质 C 覆盖了 common 返回 c,

  • D 继承了 B and C,

  • 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