Подтвердить что ты не робот

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

спасибо

4b9b3361

Ответ 1

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

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

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

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

Линеаризация A with D with C with B является

  • (вершина иерархии) A (построен сначала как базовый класс)
  • линеаризация D
    • A (не рассматривается как A встречается раньше)
    • D (D расширяет A)
  • линеаризация С
    • A (не рассматривается как A встречается раньше)
    • B (B расширяет A)
    • C (C расширяет B)
  • линеаризация B
    • A (не рассматривается как A встречается раньше)
    • B (не рассматривается как B встречается раньше)

Итак, линеаризация: ADBC. Вы могли бы думать об этом как о линейной иерархии, где 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

Ответ 2

Scala, и вы можете посмотреть их, добавив их по одному:

  • Начните с new A = > foo = "A"
  • Стек with D = > foo = "DA"
  • Стек with C, который складывает with B = > foo = "CBDA"
  • Stack with B ничего не делает, потому что B уже укладывается в C = > foo = "CBDA"

Здесь сообщение в блоге о том, как Scala решает проблему наследования алмазов.

Ответ 3

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

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

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

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

Вы видите, у вас есть результат: CBDA Поэтому ответ - CBDA.

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

Ответ 4

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

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 и получим

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

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

C B D A

super.foo в классе C вызовет foo в B и foo в B вызовет foo в D и т.д.

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

Ответ 5

, как компилятор видит класс 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);
    ()
  }
};

приведенный пример

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

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 = [email protected]

scala> new A with C with B
 A
 C
 B
res1: A with C with B = [email protected]

Итак, это основной порядок линеаризации. Таким образом, последний будет перезаписывать предыдущий.

Ваша проблема немного сложнее. Поскольку ваши черты уже расширяют другие черты, которые сами переопределяют некоторые значения предыдущих признаков. Но порядок инициализации left to right или right will override left.

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

Ответ 6

В дополнение к другим андерсерам вы можете найти пошаговое объяснение в результатах фрагмента ниже

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>

Ответ 7

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

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

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 X extends A with D with C with B
  • Итак, первым является A, так как A не имеет родителей (deadend), просто распечатайте его конструктор
  • Теперь D, который расширяет A, так как мы уже напечатали A, тогда пусть print D
  • Теперь C, который расширяет B, который расширяет A, поэтому мы пропускаем A, потому что он уже был напечатан, затем мы печатаем B, затем печатаем C (он как рекурсивный funtion)
  • Теперь B, который расширяет A, мы пропускаем A, а также пропускаем B (ничего не печатаем)
  • и вы получили 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

Надеюсь, это было достаточно просто для начинающих вроде меня.