Scala中的线性化顺序

问题描述 投票:37回答:7

使用特征时,我很难理解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
7个回答
44
投票

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

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

从而:

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

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

  • (层次结构顶部)A(首先构造为基类)
  • 线性化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,它返回一个“C”,然后是super.foo(),它在BB左边的特征,即线性化中的更高/之前的特征)上解析,返回“B”,然后返回在super.foo()上解决的D,它返回一个“D”,然后是super.foo(),它在A上解析,最终返回“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

12
投票

Scala的特征堆栈,所以你可以通过一次添加一个来查看它们:

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

这是关于Scala如何解决钻石遗传问题的blog post


12
投票

接受的答案很精彩,但为了简化起见,我想以不同的方式尽力描述它。希望可以帮助一些人。

遇到线性化问题时,第一步是绘制类和特征的层次结构树。对于此特定示例,层次结构树将是这样的:

enter image description here

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

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;

重要的是,有一个规则可以通过首先使用右先优先深度优先搜索来解析方法调用。换句话说,您应该从最右侧开始编写线性化。如下:L(B)>> L(C)>> L(D)>> L(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}

6
投票

scala解析超级调用的过程称为线性化。在您的示例中,您将对象创建为

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.

C中的super.foo将在B中调用foo,在D中调用B调用foo,等等。

附:这里l(A)是A的线性化


1
投票

除了其他的anwsers,您还可以在下面的代码段结果中找到分步说明

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>

1
投票

explanation, how the compiler sees a class Combined which extends the traits A with D with C with B

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);
    ()
  }
};

reduced example

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

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$1@5e025e70

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

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

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

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


0
投票

实际上,我看到你刚刚颠倒了构造函数的线性化,我认为这很简单,所以首先让我们理解构造函数的线性化

第一个例子

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

所以要计算输出我只是从左到右依次取类/特征然后递归写输出(没有重复)这里是如何:

  1. 我们的班级签名是:class X extends A with D with C with B
  2. 所以第一个是A,因为A没有父母(deadend)只打印它的构造函数
  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

我希望对于像我这样的初学者来说这很简单

© www.soinside.com 2019 - 2024. All rights reserved.