В чем разница между типами личности и наследованием признаков в 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().


В чем функциональная разница между победой и победой? Я получаю в обоих случаях через разные механизмы? И в вашем первом примере ... В вашем первом абзаце, почему бы не иметь черту 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 должен быть явно указан при создании экземпляров объектов.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.