Порядок линеаризации в 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 но я не могу выяснить, почему. Как определяется порядок черт?

Thx

7 ответов


интуитивный способ рассуждать о линеаризации-ссылаться на порядок построения и визуализировать линейную иерархию.

вы могли бы подумать в эту сторону. Базовый класс строится первым; но прежде чем построить базовый класс, его суперклассы/черты должны быть построены первыми (это означает, что строительство начинается в верхней части иерархии). Для каждого класса в иерархии смешанные черты строятся слева направо, потому что черта справа добавлена "позже "и, таким образом, имеет возможность" переопределить " предыдущие черты. Однако, подобно классам, чтобы построить признак, его базовые признаки должны быть построены первыми (очевидными); и, вполне разумно, если признак уже был построен (где-либо в иерархии), он не восстанавливается снова. Порядок построения-это обратная линеаризация. Подумайте о "базовых" признаках / классах как более высоких в линейной иерархии, а признаки ниже в иерархии как ближе к класс/объект, который является предметом линеаризации. Линеаризация влияет на то, как ' super разрешен в черте: он разрешится до ближайшего базового признака (выше в иерархии).

таким образом:

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

линеаризации из A with D with C with B и

  • (вершина иерархии) A (построенный сначала как базовый класс)
  • линеаризация D
    • A (не рассматривается как A происходит раньше)
    • D (D расширяет А)
  • линеаризация 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(), который решается на B (черта слева от B, т. е. выше / раньше, в линеаризации), который возвращает "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

стек признаков 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 решает алмазное наследование проблема.


процесс, с помощью которого scala разрешить супер вызов называется линеаризации В вашем примере вы создаете объект как

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

так как указано Scala reference docs здесь вызов 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 и мы get

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

здесь у нас нет никаких дубликат Теперь начинаем звонить с C

C B D A

супер.foo в класс C будем называть foo в B и фу в Б называют фу в D и так далее.

P.S. здесь l (A) - линеаризация a


принятый ответ замечательный, однако, ради упрощения, я хотел бы сделать все возможное, чтобы описать его, по-другому. Надежда может помочь некоторым людям.

когда вы сталкиваетесь с проблемой линеаризации,первый шаг это нарисовать дерево иерархии классов и признаков. Для этого конкретного примера дерево иерархии будет выглядеть примерно так:

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)

Четвертый этап самый простой шаг. Просто замените каждую линеаризацию со второго шага на третий. После замены у вас будет что-то вроде это:

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

последнее, но не менее, теперь вы должны удалить все дублирующиеся классы слева направо. Жирные символы следует удалить: D ->A - > C - > B ->A - > D ->A -> и

вы видите, у вас есть результат: C - > B - > D - > A Поэтому ответ-CBDA.

Я знаю, что это не индивидуально глубокое концептуальное описание, но может помочь в качестве дополнения для концептуальное описание, я полагаю.


в дополнение к другим 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>

объяснение, как компилятор видит класс Combined, который расширяет способности 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);
    ()
  }
};
уменьшенный

вы можете читать слева направо. Вот небольшой пример. Три признака будут печатать свое имя при инициализации, т. е. extended:

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.

вы должны иметь в виду, что сама черта будет инициализирована первой.


ну на самом деле я вижу, что вы только что отменили линеаризацию конструктора, что я думаю, довольно просто, поэтому сначала давайте поймем линеаризацию конструктора

Первый Пример

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

Я надеюсь, что это было достаточно просто для начинающих, как я