Scala 中的线性化顺序

Linearization order in Scala

在使用特征时,我很难理解 Scala 中的线性化顺序:

class A {
  def foo() = "A"
}

trait B extends A {
  override def foo() = "B" + super.foo()
}

trait C extends B {
  override def foo() = "C" + super.foo()
}

trait D extends A {
  override def foo() = "D" + super.foo()
}

object LinearizationPlayground {
    def main(args: Array[String]) {

      var d = new A with D with C with B;
      println(d.foo) // CBDA????
  }    
}

它打印 CBDA 但我不明白为什么。性状的顺序是如何确定的?

感谢

Scala 的 traits 堆栈,因此您可以一次添加一个来查看它们:

  1. new A 开始 => foo = "A"
  2. 堆栈 with D => foo = "DA"
  3. 堆栈 with C 堆栈 with B => foo = "CBDA"
  4. Stack with B 什么都不做,因为 B 已经堆叠在 C => foo = "CBDA"

这里是blog post关于Scala如何解决菱形继承问题的

解释,编译器如何看待扩展特征 A with D with C with B

的 class Combined
class Combined extends A with D with C with B {
  final <superaccessor> <artifact> def super$foo(): String = B$class.foo(Combined.this);
  override def foo(): String = C$class.foo(Combined.this);
  final <superaccessor> <artifact> def super$foo(): String = D$class.foo(Combined.this);
  final <superaccessor> <artifact> def super$foo(): String =  Combined.super.foo();
  def <init>(): Combined = {
    Combined.super.<init>();
    D$class./*D$class*/$init$(Combined.this);
    B$class./*B$class*/$init$(Combined.this);
    C$class./*C$class*/$init$(Combined.this);
    ()
  }
};

简化示例

您可以从左到右阅读。这是一个小例子。这三个特征将在初始化时打印它们的名称,即扩展:

scala> trait A {println("A")}
scala> trait B {println("B")}
scala> trait C {println("C")}

scala> new A with B with C
  A
  B
  C
res0: A with B with C = $anon@5e025e70

scala> new A with C with B
 A
 C
 B
res1: A with C with B = $anon@2ed94a8b

所以这是基本的线性化顺序。所以最后一个会覆盖上一个

你的问题有点复杂。由于您的特征已经扩展了其他特征,这些特征本身会覆盖先前特征的某些值。 但是初始化顺序left to right还是right will override left.

你必须记住,特征本身将首先被初始化。

推理线性化的一种直观方法是参考构造顺序并可视化线性层次结构。

你可以这么想。首先构建基础class;但是在能够构建基础 class 之前,必须先构建它的 superclasses/traits (这意味着构建从层次结构的顶部开始)。对于层次结构中的每个 class,混合特征是从左到右构造的,因为右侧的特征是“稍后”添加的,因此有机会“覆盖”先前的特征。但是,与 classes 类似,为了构造一个特征,必须首先构造它的基本特征(显而易见);并且,相当合理的是,如果已经构造了一个特征(在层次结构中的任何地方),则不会再次重建它。现在,构造顺序与线性化相反。将“基础”traits/classes 视为线性层次结构中的较高层次,将层次结构中较低的特征视为更接近线性化主题的 class/object。 线性化会影响“超级”在特征中的解析方式:它将解析为最接近的基本特征(在层次结构中更高)。

因此:

var d = new A with D with C with B;

A with D with C with B 的线性化是

  • (层次结构的顶部)A(首先构造为基础 class)
  • D 的线性化
    • A(不认为是之前出现的A)
    • D(D 扩展 A)
  • C 的线性化
    • A(不认为是之前出现的A)
    • B(B 扩展 A)
    • C(C 扩展 B)
  • B 的线性化
    • A(不认为是之前出现的A)
    • B(不认为之前出现过B)

所以线性化是:A-D-B-C。 您可以将其视为线性层次结构,其中 A 是根(最高)并且首先构建,C 是叶(最低)并且最后构建。由于 C 是最后构造的,这意味着可以覆盖“以前的”成员。

根据这些直观的规则,d.foo 调用 C.foo,其中 returns 一个“C”,后跟 super.foo(),在 B 上解析( B 左侧的特征,即 higher/before,在线性化中),其中 returns 一个“B”,后跟 super.foo(),在 D 上解析,其中 returns 一个“D”,然后是 super.foo(),在 A 上解析,最后是 returns“A”。所以你有“CBDA”。

再举个例子,我准备了下面的:

class X { print("X") }
class A extends X { print("A") }
trait H { print("H") }
trait S extends H { print("S") }
trait R { print("R") }
trait T extends R with H { print("T") }
class B extends A with T with S { print("B") }

new B  // X A R H T S B     (the prints follow the construction order)

// Linearization is the reverse of the construction order.
// Note: the rightmost "H" wins (traits are not re-constructed)
// lin(B) = B >> lin(S) >> lin(T) >> lin(A)
//        = B >> (S >> H) >> (T >> H >> R) >> (A >> X)
//        = B >> S >> T >> H >> R >> A >> X

除了其他答案之外,您还可以在下面的摘要结果中找到分步说明

hljs.initHighlightingOnLoad();
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.0.0/highlight.min.js"></script>
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.0.0/styles/zenburn.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
<table class="table">
  <tr>
    <th>Expression</th>
    <th>type</th>
    <th><code>foo()</code> result</th>
  </tr>

  <tr>
    <td><pre><code class="scala"> new A </code></pre>
    </td>
    <td><pre><code class="scala"> A </code></pre>
    </td>
    <td><pre><code class="scala">"A"</code></pre>
    </td>
  </tr>

  <tr>
    <td><pre><code class="scala"> new A with D </code></pre>
    </td>
    <td><pre><code class="scala"> D </code></pre>
    </td>
    <td><pre><code class="scala">"DA"</code></pre>
    </td>
  </tr>
  
    <tr>
    <td><pre><code class="scala"> new A with D with C </code></pre>
    </td>
    <td><pre><code class="scala"> D with C </code></pre>
    </td>
    <td><pre><code class="scala">"CBDA"</code></pre>
    </td>
  </tr>
  
   <tr>
    <td><pre><code class="scala"> new A with D with C with B </code></pre>
    </td>
    <td><pre><code class="scala"> D with C </code></pre>
    </td>
    <td><pre><code class="scala">"CBDA"</code></pre>
    </td>
  </tr>
</table>

scala解析super调用的过程叫做线性化 在您的示例中,您将对象创建为

var d = new A with D with C with B;

因此,如指定的 scala 参考文档 Here 对 super 的调用将解析为

l(A) = A >> l(B) >> l(c) >> l(D)

l(A) = A >> B >> l(A) >> l(C) >> l(D)

l(A) = A >> B >> A >> C >> l(B) >> l(D)

l(A) = A >> B >> A >> C >> B >> l(A) >> l(D)

l(A) = A >> B >> A >> C >> B >> A >> l(D)

l(A) = A >> B >> A >> C >> B >> A >> D >> l(A)

l(A) = A >> B >> A >> C >> B >> A >> D >> A

现在从左边开始并删除重复的结构,其中右边将获胜

例如删除 A 我们得到

l(A) = B >> C >> B >> D >> A

删除 B 我们得到

l(A) = C >> B >> D >> A

这里没有重复的条目 现在开始从 C

调用

C B D A

super.foo in class C 将在 B 中调用 foo 并且 B 中的 foo 在 D 中调用 foo 等等.

P.S。这里 l(A) 是 A

的线性化

其实我看你只是把构造函数线性化反了过来,我觉得挺简单的,所以先了解一下构造函数线性化

第一个例子

object Linearization3 {
  def main(args: Array[String]) {
    var x = new X
    println()
    println(x.foo)
  }
}

class A {
  print("A")

  def foo() = "A"
}

trait B extends A {
  print("B")

  override def foo() =   super.foo() + "B" // Hence I flipped yours to give exact output as constructor
}

trait C extends B {
  print("C")

  override def foo() =  super.foo() + "C"
}

trait D extends A {
  print("D")

  override def foo() =  super.foo() + "D"
}

class X extends A with D with C with B

输出:

ADBC
ADBC

所以为了计算输出我只是从左到右一个一个地取 class/traits 然后递归地写输出(没有重复)这里是如何:

  1. 我们的 class 签名是:class X extends A with D with C with B
  2. 所以第一个是 A,因为 A 没有父节点(死端)只打印它的构造函数
  3. 现在是D,它扩展了A,既然我们已经打印了A,那么让我们打印D
  4. 现在 C 扩展 B,扩展 A,所以我们跳过 A,因为它已经被打印,然后我们打印 B,然后打印 C(这就像一个递归函数)
  5. 现在 B,它扩展了 A,我们跳过 A,我们也跳过 B(没有打印)
  6. 你得到了 ADBC!

反向示例(您的示例)

object Linearization3 {
  def main(args: Array[String]) {
    var x = new X
    println()
    println(x.foo)
  }
}

class A {
  print("A")

  def foo() = "A"
}

trait B extends A {
  print("B")

  override def foo() = "B" + super.foo()
}

trait C extends B {
  print("C")

  override def foo() = "C" + super.foo()
}

trait D extends A {
  print("D")

  override def foo() = "D" + super.foo()
}

class X extends A with D with C with B

输出为:

ADBC
CBDA

我希望这对像我这样的初学者足够简单

接受的答案很好,但是,为了简单起见,我想尽量用不同的方式描述它。希望能帮到一些人。

遇到线性化问题,第一步是画出类和traits的hierarchy tree .对于这个特定示例,层次结构树将是这样的:

第二步是写下所有干扰目标问题的traits和类的线性化。在最后一步之前,您将需要它们。为此,您只需编写到达根目录的路径。特征的线性化如下:

L(A) = A
L(C) = C -> B -> A
L(B) = B -> A
L(D) = D -> A

第三步,写问题的线性化。在这个具体问题中,我们计划解决

的线性化

var d = new A with D with C with B;

重要的注意事项是,它通过首先使用右优先、深度优先搜索来解析方法调用。换句话说,你应该从最右边开始写线性化。它是这样的: 大号(B)>>大号(C)>>大号(D)>>大号(A)

第四步是最简单的一步。只需将每个线性化从第二步替换到第三步即可。替换后,你会得到这样的东西:

B -> A -> C -> B -> A -> D -> A -> A

最后但并非最不重要的,您现在应该从左到右删除所有重复的 类。应删除粗体字符: B -> A -> C -> B -> A -> D -> A -> A

你看,你得到了结果:C -> B -> D -> A 所以答案是CBDA。

我知道这不是单独的深度概念描述,但可以作为我猜想的概念描述的补充。


而这部分是靠公式来解释的:

 Lin(new A with D with C with B) = {A, Lin(B), Lin(C), Lin(D)}
 Lin(new A with D with C with B) = {A, Lin(B), Lin(C), {D, Lin(A)}}
 Lin(new A with D with C with B) = {A, Lin(B), Lin(C), {D, A}}
 Lin(new A with D with C with B) = {A, Lin(B), {C, Lin(B)}, {D, A}}
 Lin(new A with D with C with B) = {A, Lin(B), {C, {B, Lin(A)}}, {D, A}}
 Lin(new A with D with C with B) = {A, Lin(B), {C, {B, A}}, {D, A}}
 Lin(new A with D with C with B) = {A, {B, A}, {C, {B, A}}, {D, A}}
 Lin(new A with D with C with B) = {C,B,D,A}