В чем разница между типами личности и наследованием признаков в Scala?

9

Когда Googled, появляется много ответов на эту тему. Однако я не чувствую, что кто-либо из них хорошо иллюстрирует разницу между этими двумя функциями. Поэтому я хотел бы попробовать еще раз, в частности ...

Что можно сделать с помощью самоподтипов, а не наследования, и наоборот?

Для меня должно быть какое-то количественное, физическое различие между ними, иначе они просто номинально отличаются.

Если Черта A расширяет B или самотипы B, разве они оба не иллюстрируют, что наличие B является требованием? В чем разница?

Марк Канлас
источник
Я настороженно отношусь к условиям, которые вы определили для награды. С одной стороны, определите «физическую» разницу, учитывая, что это все программное обеспечение. Кроме того, для любого составного объекта, который вы создаете с помощью mixins, вы, вероятно, можете создать что-то приближенное по функции с наследованием - если вы определяете функцию исключительно в терминах видимых методов. Они будут различаться в расширяемости, гибкости и компоновке.
Брюс
Если у вас есть ассортимент стальных пластин разного размера, вы можете скрепить их болтами вместе, чтобы сформировать коробку, или вы можете сварить их. С одной узкой точки зрения, они будут эквивалентны по функциональности - если вы игнорируете тот факт, что один может быть легко перенастроен или расширен, а другой - нет. У меня такое чувство, что вы будете спорить, что они эквивалентны, хотя я был бы счастлив, если бы вы ошиблись, если бы вы сказали больше о своих критериях.
Брюс
Я более чем знаком с тем, что вы говорите в целом, но я все еще не понимаю, какая разница в данном конкретном случае. Не могли бы вы привести примеры кода, которые показывают, что один метод более расширяем и гибок, чем другой? * Базовый код с расширением * Базовый код с собственными типами * Функция, добавленная к стилю расширения * Функция, добавленная к стилю самостоятельного типа
Марк Канлас
Хорошо, думаю, что я могу попробовать это до того, как
закончится награда

Ответы:

11

Если черта A расширяет B, то смешивание в A дает вам точно B плюс все, что A добавляет или расширяет. Напротив, если черта A имеет собственную ссылку, которая явно указана как B, тогда родительский класс должен также смешиваться с B или с потомком B (и сначала смешивать его , что важно).

Это самое важное отличие. В первом случае точный тип B кристаллизуется в точке A, расширяющей его. Во втором случае разработчик родительского класса решает, какая версия B используется в точке, где создается родительский класс.

Другое отличие состоит в том, что A и B предоставляют методы с одинаковыми именами. Если A расширяет B, метод A переопределяет B. Когда A смешивается после B, метод A просто выигрывает.

Напечатанная ссылка на себя дает вам гораздо больше свободы; связь между A и B ослаблена.

ОБНОВИТЬ:

Поскольку вы не уверены в пользе этих различий ...

Если вы используете прямое наследование, вы создаете черту A, которая является B + A. Вы установили отношения в камне.

Если вы используете типизированную ссылку на себя, то любой, кто хочет использовать вашу черту A в классе C, может

  • Смешайте B и затем A в C.
  • Смешайте подтип B и затем A в C.
  • Смешайте A в C, где C является подклассом B.

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

Что касается разницы между выигрышем метода А , поскольку А смешивается последним, по сравнению с А, расширяющим В, рассмотрим это ...

Когда вы смешиваете в последовательности признаков, всякий раз, когда foo()вызывается метод , компилятор переходит к последней смешанной характеристике, чтобы искать foo(), затем (если не найден) он проходит последовательность влево, пока не найдет характеристику, которая реализует foo()и использует который. A также имеет опцию call super.foo(), которая также проходит по последовательности влево, пока не найдет реализацию, и так далее.

Так что, если A имеет типизированную собственную ссылку на B и автор A знает, что B реализует foo(), A может вызвать, super.foo()зная, что, если ничего не предоставит foo(), B сделает. Тем не менее, создатель класса C имеет возможность отбросить любую другую черту, в которой реализованы foo(), и A получит это вместо этого.

Опять же , это гораздо более мощные и менее жесткие ограничения , чем вытянутая B и прямой вызов версии B по foo().

itsbruce
источник
В чем функциональная разница между победой и победой? Я получаю в обоих случаях через разные механизмы? И в вашем первом примере ... В вашем первом абзаце, почему бы не иметь черту A, расширяющую SuperOfB? Такое ощущение, что мы всегда можем переделать проблему, используя любой механизм. Я предполагаю, что не вижу случая использования, где это невозможно. Или я предполагаю слишком много вещей.
Марк Канлас
Хм, почему вы хотите, чтобы A расширял подкласс B, если B определяет то, что вам нужно? Самореференция вынуждает B (или подкласс) присутствовать, но дает разработчику выбор? Они могут смешиваться во что-то, что они написали после того, как вы написали черту A, если она расширяет B. Зачем ограничивать их только тем, что было доступно, когда вы писали черту A?
Брюс
Обновлен, чтобы сделать разницу супер ясно.
Брюс
@itsbruce есть ли концептуальная разница? IS-A против HAS-A?
Jas
@Jas В контексте отношений между признаками A и B наследование является IS-A, в то время как типизированная самостоятельная ссылка дает HAS-A (композиционное отношение). Для класса, в который смешаны черты, результатом является IS-A , независимо.
Брюс
0

У меня есть некоторый код, который иллюстрирует некоторые различия между видимостью и реализацией «по умолчанию» при расширении против установки собственного типа. Он не иллюстрирует ни одну из обсуждаемых частей о том, как разрешаются реальные коллизии имен, но вместо этого сосредотачивается на том, что возможно и невозможно сделать.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

Одним из важных отличий IMO является то, что A1он не раскрывает потребности Bв чем-либо, что просто видит его A1(как показано на приведенных деталях). Единственный код, который фактически увидит, что используется конкретная специализация, B- это код, который явно знает о составном типе (например Think*{X,Y}).

Другой момент заключается в том, что A2(с расширением) будет фактически использоваться, Bесли ничего не указано, в то время как A1(self-type) не говорит, что он будет использовать, Bпока не будет переопределено, конкретный B должен быть явно указан при создании экземпляров объектов.

Rouzbeh Delavari
источник