Яка різниця між власними типами та успадкуванням ознак у Scala?


9

Коли в Google з’являється багато відповідей на цю тему. Однак я не відчуваю, що жоден з них робить гарну роботу, щоб проілюструвати різницю між цими двома можливостями. Тому я хотів би спробувати ще раз, конкретно ...

Що є те, що можна зробити за допомогою власних типів, а не з успадкуванням, і навпаки?

Для мене має бути якась кількісна фізична різниця між ними, інакше вони просто номінально різні.

Якщо ознака A поширюється на B або самовведення B, чи не обидва вони ілюструють, що буття B - це вимога? Де різниця?


Я з обережністю ставлюся до умов, які ви встановили на винагороду. Для одного визначте "фізичну" різницю, враховуючи, що це все програмне забезпечення. Крім того, для будь-якого складеного об'єкта, який ви створюєте за допомогою міксин, ви, ймовірно, можете створити щось приблизне у функції з успадкуванням - якщо ви визначаєте функцію виключно з точки зору видимих ​​методів. Де вони будуть відрізнятися, це розширюваність, гнучкість та компонування.
йогобурс

Якщо у вас асортимент сталевих пластин різного розміру, ви можете з'єднати їх між собою, щоб сформувати коробку, або ви могли їх зварити. З одного, вузького погляду, вони були б еквівалентними за функціональністю - якщо ви нехтуєте тим фактом, що один може бути легко налаштований або розширений, а другий не може. У мене є відчуття, що ви збираєтесь стверджувати, що вони рівнозначні, хоча я би радий, що мені доведеться неправильно, якщо ви сказали більше про свої критерії.
йогобурс

Мені більше ніж знайоме з тим, що ви говорите загалом, але я все ще не розумію, у чому різниця в цьому конкретному випадку. Чи можете ви навести кілька прикладів коду, які показують, що один метод є більш розтяжним та гнучким, ніж інший? * Базовий код з розширенням * Базовий код із типами самоврядування * Додана функція до стилю розширення * Функція додана до стилю
самовведення

Гаразд, подумайте, я можу спробувати це до того, як закінчиться щедрість;)
йогобурс

Відповіді:


11

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

Це найважливіша відмінність. У першому випадку точний тип В кристалізується в точці А, яка розширює його. По-друге, дизайнер батьківського класу отримує рішення про те, яку версію B використовуватиме, в точці, де складається батьківський клас.

Інша відмінність полягає в тому, де A і B надають методи з однойменною назвою. Якщо A поширюється на B, метод A переосмислює B. Якщо A замішується після B, метод A просто виграє.

Набране самонавіювання дає вам набагато більше свободи; з'єднання між А і В є вільним.

ОНОВЛЕННЯ:

Оскільки вам не зрозуміло користь цих відмінностей ...

Якщо ви використовуєте пряме успадкування, ви створюєте ознаку A, яка є B + A. Ви встановили відносини в камені.

Якщо ви використовуєте набрану самостійну посилання, будь-хто, хто хоче використовувати вашу ознаку А в класі С, міг би

  • Змішайте B, а потім A в C.
  • Змішайте підтип B, а потім A в C.
  • Змішайте A в C, де C є підкласом B.

І це не межа їх можливостей, враховуючи те, як Scala дозволяє інстанціювати ознаку безпосередньо з блоком коду як її конструктором.

Щодо різниці між виграшним методом A , оскільки A змішаний останнім, порівняно з A, що розширюється B, врахуйте це ...

Якщо ви змішуєте в послідовності ознак, кожного разу, коли використовується метод foo(), компілятор переходить до останньої ознаки, змішаної, щоб шукати foo(), тоді (якщо її не знайдено), вона переміщає послідовність зліва, поки не знайде ознаку, яка реалізує foo()та використовує що. Також є можливість викликати super.foo(), який також проходить послідовність зліва, поки не знайде реалізацію тощо.

Отже, якщо A має введене само посилання на B, і письменник A знає, що B реалізує foo(), A може зателефонувати, super.foo()знаючи, що якщо нічого іншого не передбачено foo(), B буде. Однак у творця класу C є можливість скинути будь-яку іншу ознаку, в якій реалізується foo(), і A отримає це замість цього.

Знову ж , це набагато більш потужні і менш жорсткі обмеження , ніж витягнута B і прямий виклик версії B по foo().


У чому полягає функціональна відмінність перемоги від перемоги? Я отримую A в обох випадках за допомогою різних механізмів? І у вашому першому прикладі ... У першому пункті чому б не мати ознаку A Extender SuperOfB? Просто відчувається, що ми завжди могли б переробити проблему за допомогою будь-якого механізму. Я думаю, я не бачу випадків використання, коли це неможливо. Або я припускаю занадто багато речей.
Марк Канлас

Гм, чому б ви хотіли, щоб A розширював підклас B, якщо B визначає, що вам потрібно? Самопосилання змушує B (або підклас) бути присутнім, але дає можливість розробнику tbe? Вони можуть змішуватись у тому, що вони написали після того, як ви написали рисунок A, якщо він поширюється на B. Чому варто обмежувати їх лише тим, що було доступно, коли ви писали рисунок A?
йогобрюс

Оновлено, щоб зробити різницю дуже зрозумілою.
йогобрюс

@itsbruce чи є концептуальна різниця? IS-A проти HAS-A?
Жас

@Jas У контексті взаємозв'язку між ознаками A і B , успадкування IS-A, тоді як типізоване самовідсилання дає HAS-A (композиційне відношення). Для класу, до якого змішуються риси, результат - IS-A , незалежно.
йогобрюс

0

У мене є якийсь код, який ілюструє деякі відмінності видимості wrt та "за замовчуванням" реалізацій при розширенні проти встановлення самовведення. Це не ілюструє жодну з обговорених частин про те, як вирішуються фактичні зіткнення імен, а натомість зосереджується на тому, що можна, а що неможливо зробити.

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
}

Важлива відмінність ІМО полягає в тому, що A1він не підкреслює, що він потрібен Bнічому, що просто розглядає його як A1(як це показано в оновлених частинах). Єдиний код, який фактично побачить, що використовується конкретна спеціалізація, B- це код, який явно знає про складений тип (наприклад Think*{X,Y}).

Інший момент полягає в тому, що A2(з розширенням) насправді використовуватиметься, Bякщо нічого іншого не вказано, тоді як A1(самовведення) не говорить, що він буде використовуватись, Bякщо не буде відмінено, конкретний B повинен бути явно наданий, коли об'єкти інстанціюються.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.