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


387

Самонабір для ознаки A:

trait B
trait A { this: B => }

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

З іншого боку, наступне:

trait B
trait A extends B

говорить, що "будь-яке (конкретний чи абстрактний) клас змішування Aтакож буде змішуватися в B" .

Чи не означають ці два твердження одне і те ж? Схоже, що власний тип служить лише для створення можливості простої помилки компіляції.

Що я пропускаю?


Мене насправді цікавлять відмінності між типами "само" та підкласифікацією ознак. Я знаю деякі поширені способи використання для власних типів; Я просто не можу знайти причину, чому вони не будуть більш чітко виконані так само, як і підтиснення.
Дейв

32
Можна використовувати параметри типів у межах власних типів: trait A[Self] {this: Self => }законно, trait A[Self] extends Selfні.
Blaisorblade

3
Тип типу також може бути класом, але ознака не може успадковувати клас.
cvogt

10
@cvogt: ознака може успадковувати клас (принаймні 2.10): pastebin.com/zShvr8LX
Ерік Каплун

1
@Blaisorblade: Хіба це не те, що могло б бути вирішене невеликим перетворенням мови, хоча і не є принциповим обмеженням? (принаймні з точки зору питання)
Ерік Каплун

Відповіді:


273

Він переважно використовується для ін'єкцій залежностей , наприклад, у схемі торта. Існує чудова стаття, що висвітлює багато різних форм введення залежності в Scala, включаючи схему торта. Якщо ви "Google Cake Pattern and Scala", ви отримаєте багато посилань, включаючи презентації та відео. На даний момент ось посилання на інше питання .

Тепер, у чому різниця між типом "Я" і розширенням ознаки, це просто. Якщо ви говорите B extends A, то B цеA . Якщо ви використовуєте власні типи, B потрібно мати A. Існують дві конкретні вимоги, які створюються за допомогою власних типів:

  1. Якщо його Bбуде розширено, вам потрібно вписати A.
  2. Коли конкретний клас остаточно розширює / змішує ці ознаки, деякий клас / ознака повинен реалізовуватися A.

Розглянемо наступні приклади:

scala> trait User { def name: String }
defined trait User

scala> trait Tweeter {
     |   user: User =>
     |   def tweet(msg: String) = println(s"$name: $msg")
     | }
defined trait Tweeter

scala> trait Wrong extends Tweeter {
     |   def noCanDo = name
     | }
<console>:9: error: illegal inheritance;
 self-type Wrong does not conform to Tweeter's selftype Tweeter with User
       trait Wrong extends Tweeter {
                           ^
<console>:10: error: not found: value name
         def noCanDo = name
                       ^

Якби Tweeterбув підклас User, помилок не було б. У наведеному вище коді, ми обов'язковоUser щоразу , коли Tweeterвикористовується, однак Userне було надано Wrong, тому ми отримали помилку. Тепер, коли код все ще знаходиться в області застосування, врахуйте:

scala> trait DummyUser extends User {
     |   override def name: String = "foo"
     | }
defined trait DummyUser

scala> trait Right extends Tweeter with User {
     |   val canDo = name
     | }
defined trait Right 

scala> trait RightAgain extends Tweeter with DummyUser {
     |   val canDo = name
     | }
defined trait RightAgain

З Right, вимога до змішування a Userзадовольняється. Однак друга вимога, згадана вище, не виконується: тягар впровадження Userвсе ще залишається за класами / ознаками, які поширюються Right.

З RightAgainзадовольняються обидві вимоги. Забезпечено виконання Userта реалізація User.

Більш практичні випадки використання див. За посиланням на початку цієї відповіді! Але, сподіваємось, тепер ви це отримаєте.


3
Дякую. Шаблон "Торт" - це 90% того, що я маю на увазі, чому я говорю про ажіотаж навколо власних типів ... саме там я вперше побачив цю тему. Приклад Йонаса Бонера чудовий, оскільки він підкреслює точку мого запитання. Якщо ви змінили власні типи в прикладі нагрівача, щоб вони були підрахунками, то яка буде відмінність (крім помилки, яку ви отримуєте при визначенні ComponentRegistry, якщо ви не змішуєте потрібні речі?
Дейв

29
@Dave: Ви маєте на увазі, як trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Це спричинило WarmerComponentImplб наявність цих інтерфейсів. Вони були б доступні для всього, що розширено WarmerComponentImpl, що явно неправильно, оскільки це не є SensorDeviceComponentані ані OnOffDeviceComponent. Як тип самостійної, ці залежності доступні виключно для WarmerComponentImpl. A Listможе використовуватися як Arrayі навпаки. Але вони просто не те саме.
Даніель К. Собрал

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

11
@ Родні Ні, не повинно. Насправді, з використанням thisтипів "я" я те, на що я звертаю увагу, оскільки це тіні без поважних причин оригінал this.
Даніель К. Собрал

9
@opensas Спробуйте self: Dep1 with Dep2 =>.
Даніель К. Собрал

156

Типи "Я" дозволяють визначати циклічні залежності. Наприклад, ви можете досягти цього:

trait A { self: B => }
trait B { self: A => }

Успадкування використання extendsцього не дозволяє. Спробуйте:

trait A extends B
trait B extends A
error:  illegal cyclic reference involving trait A

У книзі Одерського подивіться розділ 33.5 (Створення глави інтерфейсу електронних таблиць), де в ньому згадується:

У прикладі електронних таблиць клас Model успадковує від Оцінювача і, таким чином, отримує доступ до свого методу оцінювання. Щоб піти в інший бік, клас Оцінювач визначає свій тип власності як Model, наприклад:

package org.stairwaybook.scells
trait Evaluator { this: Model => ...

Сподіваюсь, це допомагає.


3
Я не розглядав цей сценарій. Це перший приклад того, що я бачив, що це не те саме, що самотип, як це з підкласом. Однак це видається краєм-кейсі, і, що ще важливіше, це здається поганою ідеєю (я, як правило, далеко не можу визначати циклічні залежності!). Чи вважаєте Ви це найважливішим відмінністю?
Дейв

4
Я думаю так. Я не бачу жодної іншої причини, чому я віддав би перевагу власним типам розширенню пропозиції. Самотипи є багатослівними, вони не передаються у спадок (тому ви повинні додавати власні типи до всіх підтипів як ритуал), і ви можете бачити лише член, але не можете їх замінити. Я добре знаю схему торта та багато постів, де згадуються власні типи для DI. Але я якось не переконаний. Я створив тут зразковий додаток ( bitbucket.org/mushtaq/scala-di ). Перегляньте конкретно папку / src / configs. Я досяг DI, щоб замінити складні пружинні конфігурації без власних типів.
Муштак Ахмед

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

@ DanielC.Sobral, можливо, дякує вашому коментарю, але на даний момент у нього більше відгуків, ніж у вашої програми. Обновлення обох :)
rintcius

Чому б просто не створити одну ознаку AB? Оскільки риси A і B завжди повинні поєднуватися в будь-якому заключному класі, навіщо їх розділяти в першу чергу?
Багатий Олівер

56

Ще однією додатковою відмінністю є те, що власні типи можуть задавати некласові типи. Наприклад

trait Foo{
   this: { def close:Unit} => 
   ...
}

Тип самості тут структурний тип. Ефект полягає в тому, щоб сказати, що все, що змішується у Foo, повинно реалізовувати модуль повернення одиниці методу no-arg. Це дозволяє отримати безпечні міксини для набору качок.


41
Насправді ви можете використовувати спадщину і з структурними типами: абстрактний клас A поширюється {def close: Unit}
Адріан

12
Я думаю, що структурна типізація використовує рефлексію, тому використовуйте лише тоді, коли іншого вибору немає ...
Eran Medan

@ Адріан, я вважаю, що ваш коментар невірний. `абстрактний клас A поширюється {def close: Unit}` - це просто абстрактний клас із суперкласом Object. це просто дозвільний синтаксис Скали до безглуздих виразів. Ви можете `клас X розширює {def f = 1}; Наприклад, новий X (). f`
Олексій

1
@ Алекс, я не бачу, чому твій приклад (або мій) безглуздий.
Адріан

1
@ Адріан, abstract class A extends {def close:Unit}еквівалентно abstract class A {def close:Unit}. Отже, це не передбачає структурних типів.
Олексій

13

Розділ 2.3 «Анотації на Selftype» оригінальної книги Scalable Component Scalable Component Martin Мартина Одерського насправді пояснює мету selftype за межами композиції mixin: запропонувати альтернативний спосіб асоціації класу з абстрактним типом.

Приклад, наведений у статті, виглядав наступним чином, і він, схоже, не має елегантного кореспондента підкласу:

abstract class Graph {
  type Node <: BaseNode;
  class BaseNode {
    self: Node =>
    def connectWith(n: Node): Edge =
      new Edge(self, n);
  }
  class Edge(from: Node, to: Node) {
    def source() = from;
    def target() = to;
  }
}

class LabeledGraph extends Graph {
  class Node(label: String) extends BaseNode {
    def getLabel: String = label;
    def self: Node = this;
  }
}

Для тих, хто цікавиться, чому підкласинг не вирішить це, розділ 2.3 також говорить так: «Кожен з операндів змішаної композиції C_0 з ... з C_n повинен відноситися до класу. Механізм композиції міксину не дозволяє жодному C_i звертатися до абстрактного типу. Це обмеження дозволяє статично перевіряти двозначності та переосмислювати конфлікти в точці, де складається клас ».
Люк Маурер

12

Ще одна річ, про яку не згадувалося: оскільки власні типи не є частиною ієрархії необхідного класу, їх можна виключити з відповідності шаблонів, особливо коли ви вичерпно співпадаєте із запечатаною ієрархією. Це зручно, коли ви хочете моделювати ортогональні форми поведінки, такі як:

sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition

val p : Person = new Student {}
p match {
  case s : Student => println("a student")
  case t : Teacher => println("a teacher")
} // that's it we're exhaustive

10

TL; DR резюме інших відповідей:

  • Типи, які ви поширюєте, піддаються успадкованим типам, але власні типи - ні

    наприклад: class Cow { this: FourStomachs }дозволяє використовувати методи, доступні лише жуйним тваринам, наприклад digestGrass. Риси, які поширюють Корову, не матимуть таких привілеїв. З іншого боку, class Cow extends FourStomachsбуде виставляти digestGrassвсіх, хто кого extends Cow .

  • власні типи допускають циклічні залежності, розширення інших типів не робить


9

Почнемо з циклічної залежності.

trait A {
  selfA: B =>
  def fa: Int }

trait B {
  selfB: A =>
  def fb: String }

Однак модульність цього рішення не настільки велика, як це може здатися вперше, тому що ви можете переосмислити власні типи таким чином:

trait A1 extends A {
  selfA1: B =>
  override def fb = "B's String" }
trait B1 extends B {
  selfB1: A =>
  override def fa = "A's String" }
val myObj = new A1 with B1

Хоча, якщо ви перекриєте члена типу самоврядування, ви втрачаєте доступ до початкового члена, до якого все ще можна отримати доступ через супер, використовуючи успадкування. Отже, що насправді отримується за рахунок використання спадщини:

trait AB {
  def fa: String
  def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }        
trait B1 extends AB
{ override def fb = "B's String" }    
val myObj = new A1 with B1

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

Версія успадкування коротша, але головна причина, чому я віддаю перевагу успадковуванню над типами самозрозуміння, полягає в тому, що я вважаю набагато складнішим правильний порядок ініціалізації правильним із типами self. Однак є деякі речі, які ви можете зробити із типами самозабезпечення, які ви не можете зробити із спадщиною. Типи самоврядування можуть використовувати тип, тоді як для успадкування потрібна ознака або клас, як у:

trait Outer
{ type T1 }     
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.

Ви навіть можете зробити:

trait TypeBuster
{ this: Int with String => }

Хоча ви ніколи не зможете це створити. Я не бачу жодної абсолютної причини для неможливості успадкування від типу, але я, безумовно, вважаю, що було б корисно мати класи конструкторів контурів і риси, як у нас є риси / класи конструктора типу. Як на жаль

trait InnerA extends Outer#Inner //Doesn't compile

У нас це є:

trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }

Або це:

  trait Outer
  { trait Inner }     
  trait InnerA
  {this: Outer#Inner =>}
  trait InnerB
  {this: Outer#Inner =>}
  trait OuterFinal extends Outer
  { val myVal = new InnerA with InnerB with Inner }

Один момент, якому слід більше співпереживати, - це те, що риси можуть поширювати класи. Дякуємо Девіду Макльверу за те, що вказав на це. Ось приклад з мого власного коду:

class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }    
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]

ScnBaseуспадковується від класу Swing Frame, тому він може бути використаний як тип власного типу, а потім змішаний у кінці (при встановленні). Однак val geomRйого потрібно ініціалізувати перед тим, як його використовувати, успадковуючи риси. Тож нам потрібен клас для виконання попередньої ініціалізації geomR. Потім клас ScnVistaможе бути успадкований за допомогою декількох ортогональних ознак, від яких вони можуть бути успадковані. Використання декількох параметрів типу (generics) пропонує альтернативну форму модульності.


7
trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}

// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10

// 2.
trait X {
  type SomeA <: A
  trait Inner1 { this: SomeA => } // compiles ok
  trait Inner2 extends SomeA {} // doesn't compile
}

4

Самостійний тип дає змогу вказати, які типи дозволено поєднувати ознаку. Наприклад, якщо у вас є ознака з власним типом Closeable, то ця ознака знає, що єдині речі, дозволені для змішування, повинні реалізувати Closeableінтерфейс.


3
@Blaisorblade: Цікаво, чи може ви неправильно прочитали відповідь кікібобо - тип власного ознаки дійсно дозволяє вам обмежувати типи, які можуть змішувати його, і це є частиною його корисності. Наприклад, якщо ми визначимо, trait A { self:B => ... }то декларація X with Aсправедлива лише в тому випадку, якщо X поширюється на B. Так, ви можете сказати X with A with Q, де Q не поширюється на B, але я вважаю, що точка kikibobo полягала в тому, що X настільки обмежений. Або я щось пропустив?
AmigoNico

1
Спасибі, ти маєш рацію. Моє голосування було заблоковано, але, на щастя, я зміг відредагувати відповідь, а потім змінити свій голос.
Blaisorblade

1

Оновлення: Принципова відмінність полягає в тому, що власні типи можуть залежати від декількох класів (я визнаю, це трохи кутовий випадок). Наприклад, можна мати

class Person {
  //...
  def name: String = "...";
}

class Expense {
  def cost: Int = 123;
}

trait Employee {
  this: Person with Expense =>
  // ...

  def roomNo: Int;

  def officeLabel: String = name + "/" + roomNo;
}

Це дозволяє додавати Employeeміксин просто до всього, що є підкласом Personі Expense. Звичайно, це має сенс лише, якщо Expenseпоширюється Personчи навпаки. Справа в тому, що використання самотипів Employeeможе бути незалежним від ієрархії класів, від якої це залежить. Не байдуже, що розширює, що - Якщо ви переключите ієрархію Expensevs Person, вам не доведеться змінювати Employee.


Співробітникові не потрібно бути класом, щоб сходити з Особи. Особливості можуть поширювати класи. Якщо б особливість працівника розширила особу замість використання типу самоврядування, приклад все-таки спрацював би. Я вважаю ваш приклад цікавим, але він, схоже, не ілюструє випадок використання для типів самоврядування.
Морган Крейтон

@MorganCreighton Справедливо, я не знав, що риси можуть поширювати класи. Я подумаю про це, якщо зможу знайти кращий приклад.
Петро Пудлак

Так, це дивна мовна особливість. Якщо б присвоїти співробітника розширеного класу особу, то будь-який клас, зрештою, "відзначився", працівник також повинен був би поширити особу. Але це обмеження все ще існує, якщо працівник використовував власний тип замість того, щоб розширювати особу. Ура, Петре!
Морган Крейтон

1
Я не бачу, чому "це має сенс лише в тому випадку, якщо Витрати поширюють особу чи навпаки".
Робін Грін

0

у першому випадку підряд чи підклас B можна змішати з будь-яким використанням А. Отже, B може бути абстрактною ознакою.


Ні, B може бути (і справді є) "абстрактною ознакою" в обох випадках. Тому немає різниці з цієї точки зору.
Робін Грін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.