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


387

Тип личности для черты A:

trait B
trait A { this: B => }

говорит, что « Aнельзя смешивать в конкретный класс, который также не расширяется B» .

С другой стороны, следующее:

trait B
trait A extends B

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

Разве эти два утверждения не означают одно и то же? Кажется, что self-type служит только для того, чтобы создать возможность простой ошибки во время компиляции.

Что мне не хватает?


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

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

3
Тип self также может быть классом, но черта не может наследоваться от класса.
cvogt

10
@cvogt: черта может наследоваться от класса (по крайней мере, с 2.10): pastebin.com/zShvr8LX
Эрик Каплун,

1
@Blaisorblade: разве это не то, что можно решить с помощью небольшого изменения языка, а не фундаментальное ограничение? (хотя бы с точки зрения вопроса)
Эрик Каплун

Ответы:


273

Он преимущественно используется для инъекций зависимости , например, в Cake Pattern. В Scala есть отличная статья, рассказывающая о различных формах внедрения зависимостей, включая Cake Pattern. Если вы используете 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всякий раз, когда Tweetera 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этом требование к смешиванию Userудовлетворяется. Однако второе требование, упомянутое выше, не выполняется: бремя реализации Userвсе еще остается для классов / признаков, которые расширяются Right.

С RightAgainудовлетворяются оба требования. А Userи реализация Userпредоставляются.

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


3
Спасибо. Шаблон Cake - это 90% того, что я имею в виду, почему я говорю о шумихе вокруг самоподобных типов ... именно там я впервые увидел тему. Пример Джонаса Бонера великолепен, потому что он подчеркивает суть моего вопроса. Если бы вы изменили типы самости в его примере с подогревателем, чтобы они стали субтитрами, тогда в чем будет разница (кроме ошибки, получаемой при определении ComponentRegistry, если вы не смешиваете нужные вещи?
Дейв

29
@Dave: Ты имеешь в виду как trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Это должно было бы WarmerComponentImplиметь эти интерфейсы. Они будут доступны для всего, что расширено WarmerComponentImpl, что явно неправильно, так как это не a SensorDeviceComponent, ни a OnOffDeviceComponent. Как тип self, эти зависимости доступны исключительно для WarmerComponentImpl. А Listможно использовать как Arrayи наоборот. Но они просто не одно и то же.
Даниэль С. Собрал

10
Спасибо Даниэль. Это, наверное, главное различие, которое я искал. Практическая проблема заключается в том, что использование подклассов приведет к утечке функциональности в ваш интерфейс, что вы не намерены. Это результат нарушения более теоретического правила «является частью» для черт. Self-типы выражают отношения «использует-a» между частями.
Дэйв

11
@ Родни Нет, не должно. На самом деле, использование thisс типами self - это то, на что я смотрю свысока, так как это без тени причины скрывает оригинал 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 наследуется от Evaluator и, таким образом, получает доступ к его методу оценки. Чтобы пойти другим путем, класс Evaluator определяет свой собственный тип как Model:

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

Надеюсь это поможет.


3
Я не рассматривал этот сценарий. Это первый пример того, что я видел, и это не то же самое, что и самоподтип, как с подклассом. Тем не менее, это выглядит как крайний случай и, что более важно, кажется плохой идеей (я обычно стараюсь изо всех сил НЕ определять циклические зависимости!). Считаете ли вы, что это самое важное различие?
Дэйв

4
Я думаю так. Я не вижу никакой другой причины, по которой я бы предпочел, чтобы self-types расширял предложение. Self-типы являются многословными, они не наследуются (поэтому вы должны добавить self-типы ко всем подтипам как ритуал), и вы можете видеть только участника, но не можете его переопределить. Я хорошо знаком с паттерном Cake и многими постами, в которых упоминается самопечатание для DI. Но почему-то я не убежден. Я давно создал образец приложения ( bitbucket.org/mushtaq/scala-di ). Посмотрите конкретно на папку / src / configs. Я достиг DI, чтобы заменить сложные конфигурации Spring без самостоятельных типов.
Муштак Ахмед

Муштак, мы согласны. Я думаю, что заявление Дэниела о недопущении непреднамеренной функциональности является важным, но, как вы говорите, существует зеркальное представление об этой «функции» ... что вы не можете переопределить эту функциональность или использовать ее в будущих подклассах. Это довольно ясно говорит мне, когда дизайн будет призывать один над другим. Я буду избегать самоподтипов, пока не найду подлинной необходимости - то есть, если я начну использовать объекты в качестве модулей, как указывает Дэниел. Я автоматически связываю зависимости с неявными параметрами и простым объектом начальной загрузки. Мне нравится простота.
Дэйв

@ DanielC.Sobral, возможно, благодаря вашему комментарию, но в данный момент у него больше голосов, чем у вашего ответчика. Upvoting оба :)
rintcius

Почему бы просто не создать одну черту AB? Так как черты A и B всегда должны сочетаться в любом последнем классе, зачем выделять их в первую очередь?
Рич Оливер

56

Еще одно отличие состоит в том, что self-типы могут указывать не классовые типы. Например

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

Тип self здесь является структурным типом. Эффект заключается в том, что все, что смешивается в Foo, должно реализовывать безошибочный метод «close», возвращающий единицу. Это позволяет использовать безопасные миксины для утки.


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

12
Я думаю, что структурная типизация использует рефлексию, поэтому используйте ее только тогда, когда другого выбора нет ...
Эран Медан

@ Адриан, я думаю, твой комментарий неверный. `абстрактный класс A extends {def close: Unit}` - это просто абстрактный класс с суперклассом Object. это просто разрешительный синтаксис Scala для бессмысленных выражений. Вы можете `класс X extends {def f = 1}; новый X (). f` например
Алексей

1
@ Алексей Я не понимаю, почему твой пример (или мой) бессмысленен.
Адриан

1
@Adrian, abstract class A extends {def close:Unit}эквивалентно abstract class A {def close:Unit}. Так что это не касается структурных типов.
Алексей

13

Раздел 2.3 «Аннотации к самопечатанию» оригинальной Scala-статьи Мартина Одерского « Масштабируемые абстракции компонентов» выполненной Мартином Одерским, на самом деле очень хорошо объясняет цель автотипа помимо миксиновой композиции: предоставляет альтернативный способ связывания класса с абстрактным типом.

Пример, приведенный в статье, был похож на следующий, и у него, похоже, нет элегантного корреспондента подкласса:

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 также говорится: «Каждый из операндов композиции mixin C_0 с ... с C_n должен ссылаться на класс. Механизм композиции mixin не позволяет любому 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 .

  • self-типы допускают циклические зависимости, расширение других типов не допускает


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

Хотя, если вы переопределите элемент собственного типа, вы потеряете доступ к исходному элементу, доступ к которому по-прежнему возможен через super с использованием наследования. Итак, что действительно получено за счет наследования:

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, заключается в том, что мне гораздо сложнее получить правильный порядок инициализации с типами self. Однако есть некоторые вещи, которые вы можете делать с типами себя, которые вы не можете делать с наследованием. 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может быть унаследован от нескольких ортогональных признаков, которые сами могут наследоваться. Использование параметров нескольких типов (обобщений) предлагает альтернативную форму модульности.


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

Self-тип позволяет вам указать, какие типы могут смешиваться в признаке. Например, если у вас есть черта с собственным типом Closeable, то эта черта знает, что единственные вещи, которые могут смешивать ее, должны реализовывать Closeableинтерфейс.


3
@Blaisorblade: Интересно, возможно, вы неправильно прочитали ответ Кикибобо - тип самости черты действительно позволяет вам ограничивать типы, которые могут его смешивать, и это является частью его полезности. Например, если мы определим, trait A { self:B => ... }то объявление X with Aдействительно только в том случае, если X расширяет B. Да, вы можете сказать X with A with Q, где Q не расширяет B, но я считаю, что точка зрения Кикибобо заключалась в том, что 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или наоборот. Дело в том, что использование self-типов Employeeможет быть независимым от иерархии классов, от которых она зависит. Это не заботится о том, что расширяет что - Если вы переключаете иерархию Expensevs Person, вам не нужно изменять Employee.


Сотрудник не должен быть классом, чтобы спуститься с человека. Черты могут расширять классы. Если черта Employee расширила Person вместо использования собственного типа, пример все равно будет работать. Я нахожу ваш пример интересным, но, похоже, он не иллюстрирует сценарий использования для типов себя.
Морган Крейтон

@MorganCreighton Справедливо, я не знал, что черты могут расширять классы. Я подумаю об этом, если смогу найти лучший пример.
Петр Пудлак

1
Да, это удивительная языковая особенность. Если черта Employee расширила класс Person, то любой класс, в конечном счете «увязший» в Employee, также должен был бы расширить Person. Но это ограничение все еще присутствует, если Сотрудник использовал собственный тип вместо расширения Person. Ура, Петр!
Морган Крейтон

1
Я не понимаю, почему «это имеет смысл только в том случае, если расходы увеличивают личность или наоборот».
Робин Грин

0

в первом случае под-признак или подкласс B может быть смешан с тем, что использует A. Таким образом, B может быть абстрактным признаком.


Нет, B может быть (и действительно является) «абстрактной чертой» в обоих случаях. Так что нет никакой разницы с этой точки зрения.
Робин Грин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.