Jaka jest różnica między podklasami typu własnego a cechą?

387

Typ własny dla cechy A:

trait B
trait A { this: B => }

mówi, że Anie można mieszać w konkretną klasę, która również się nie rozszerza B .

Z drugiej strony, następujące:

trait B
trait A extends B

mówi, że „dowolne (konkretne lub abstrakcyjne) mieszanie Abędzie również mieszać w B” .

Czy te dwa stwierdzenia nie oznaczają tego samego? Typ własny wydaje się służyć jedynie do stworzenia możliwości wystąpienia prostego błędu czasu kompilacji.

czego mi brakuje?

Dave
źródło
Właściwie jestem tutaj zainteresowany różnicami między typami własnymi a podklasą cech. Znam niektóre z typowych zastosowań własnych typów; Po prostu nie mogę znaleźć powodu, dla którego nie byłyby one wyraźniej wykonane w ten sam sposób z podtypami.
Dave
32
W typach własnych można używać parametrów typu: trait A[Self] {this: Self => }jest legalne, trait A[Self] extends Selfnie jest.
Blaisorblade,
3
Typ własny może być również klasą, ale cecha nie może odziedziczyć po klasie.
cvogt
10
@cvogt: cecha może odziedziczyć po klasie (przynajmniej od 2.10): pastebin.com/zShvr8LX
Erik Kaplun
1
@ Blaisorblade: czy nie jest to coś, co można by rozwiązać poprzez przebudowę małego języka, a nie podstawowe ograniczenie? (przynajmniej z punktu widzenia pytania)
Erik Kaplun

Odpowiedzi:

273

Jest używany głównie do wstrzykiwania zależności , na przykład we wzorze ciasta. Istnieje świetny artykuł obejmujący wiele różnych form wstrzykiwania zależności w Scali, w tym wzorzec Cake. Jeśli użyjesz Google „Cake Pattern and Scala”, otrzymasz wiele linków, w tym prezentacje i filmy. Na razie tutaj jest link do innego pytania .

Co do różnicy między typem własnym a rozszerzeniem cechy, to jest proste. Jeśli powiesz B extends A, to B jestA . Podczas korzystania z self-typy, B wymagaA . Istnieją dwa specyficzne wymagania, które są tworzone przy użyciu typów własnych:

  1. Jeśli Bzostanie przedłużony, to jesteś zobowiązany do mix-w sposób A.
  2. Kiedy konkretna klasa w końcu rozszerza / miesza te cechy, pewna klasa / cecha musi się zaimplementować A.

Rozważ następujące przykłady:

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
                       ^

Gdyby Tweeterbyła podklasą User, nie byłoby błędu. W powyższym kodzie wymagaliśmy użycia Userza każdym razem Tweeter, ale Usernie było to przewidziane Wrong, więc wystąpił błąd. Teraz, gdy powyższy kod jest nadal w zasięgu, rozważ:

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

Dzięki Rightspełniony jest wymóg mieszania User. Jednak drugi wymóg wspomniany powyżej nie jest spełniony: ciężar wdrażania Userwciąż spoczywa na klasach / cechach, które się rozciągają Right.

Ze RightAgainoba wymagania są spełnione. A Useri implementacja Usersą zapewnione.

Więcej praktycznych przypadków użycia znajdują się na początku tej odpowiedzi! Ale mam nadzieję, że teraz to rozumiesz.

Daniel C. Sobral
źródło
3
Dzięki. Wzór Cake to 90% tego, co mam na myśli, dlaczego mówię o szumie wokół autoportretów ... to tam po raz pierwszy zobaczyłem ten temat. Przykład Jonasa Bonera jest świetny, ponieważ podkreśla sens mojego pytania. Jeśli zmieniłeś typ własny w jego przykładzie grzejnika na podklucze, to jaka byłaby różnica (oprócz błędu, który pojawia się podczas definiowania ComponentRegistry, jeśli nie wmieszasz właściwych rzeczy?
Dave
29
@Dave: Masz na myśli trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? To spowodowałoby WarmerComponentImplposiadanie tych interfejsów. Będą one dostępne do niczego, że przedłużony WarmerComponentImpl, co jest wyraźnie nie tak, jak to jest nieSensorDeviceComponent , ani OnOffDeviceComponent. Zależności te są dostępne tylko dla siebie WarmerComponentImpl. A Listmoże być użyte jako Arrayi odwrotnie. Ale po prostu to nie to samo.
Daniel C. Sobral
10
Dzięki Daniel. Jest to prawdopodobnie główne wyróżnienie, którego szukałem. Praktyczny problem polega na tym, że użycie podklas powoduje wyciek do twojego interfejsu funkcjonalności, której nie chcesz. Jest to wynikiem naruszenia bardziej teoretycznej zasady „jest częścią” cech. Typy własne wyrażają relację „używa-a” między częściami.
Dave
11
@Rodney Nie, nie powinno. W rzeczywistości korzystanie thisz autoportretów jest czymś, na co patrzę z góry, ponieważ cienie bez wyraźnego powodu oryginału this.
Daniel C. Sobral
9
@opensas Try self: Dep1 with Dep2 =>.
Daniel C. Sobral,
156

Typy własne pozwalają definiować zależności cykliczne. Na przykład możesz to osiągnąć:

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

Korzystanie z dziedziczenia extendsnie pozwala na to. Próbować:

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

W książce Odersky spójrz na rozdział 33.5 (Tworzenie interfejsu użytkownika arkusza kalkulacyjnego), w którym wspomniano:

W przykładzie z arkusza kalkulacyjnego klasa Model dziedziczy z Evaluator, a zatem uzyskuje dostęp do swojej metody oceny. Aby przejść w drugą stronę, klasa Evaluator definiuje swój typ własny jako Model, w następujący sposób:

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

Mam nadzieję że to pomoże.

Mushtaq Ahmed
źródło
3
Nie rozważyłem tego scenariusza. Jest to pierwszy przykład czegoś, co widziałem, co nie jest tym samym co typ własny, jak w przypadku podklasy. Wydaje się to jednak nieco przewrotne i, co ważniejsze, wydaje się złym pomysłem (zwykle robię wszystko, co w mojej mocy, aby NIE definiować zależności cyklicznych!). Czy uważasz to za najważniejsze wyróżnienie?
Dave
4
Chyba tak. Nie widzę żadnego innego powodu, dla którego wolałbym, aby typy własne rozszerzyły klauzulę. Typy własne są pełne, nie są dziedziczone (więc rytuał trzeba dodawać do wszystkich podtypów) i można zobaczyć tylko członka, ale nie można ich zastąpić. Jestem w pełni świadomy wzoru Cake i wielu postów wspominających o typach własnych dla DI. Ale jakoś nie jestem przekonany. Już dawno stworzyłem tutaj przykładową aplikację ( bitbucket.org/mushtaq/scala-di ). Spójrz w szczególności na folder / src / configs. Osiągnąłem DI, aby zastąpić złożone konfiguracje sprężynowe bez typów własnych.
Mushtaq Ahmed
Mushtaq, jesteśmy zgodni. Myślę, że wypowiedź Daniela o nieujawnianiu niezamierzonej funkcjonalności jest ważna, ale, jak to ująłeś, istnieje lustrzane odbicie tej „funkcji” ... że nie możesz przesłonić funkcjonalności ani użyć jej w przyszłych podklasach. To dość wyraźnie mówi mi, kiedy projekt będzie wymagał jednego od drugiego. Będę unikać pisania własnego, dopóki nie znajdę prawdziwej potrzeby - tj. Jeśli zacznę używać obiektów jako modułów, jak wskazuje Daniel. Jestem autowire zależnościami z niejawnymi parametrami i prostym obiektem bootstrapper. Lubię prostotę.
Dave
@ DanielC.Sobral może być dzięki twojemu komentarzowi, ale w tej chwili ma więcej głosów pozytywnych niż twój anser. Upvoting oba :)
rintcius
Dlaczego nie stworzyć jednej cechy AB? Ponieważ cechy A i B muszą zawsze być łączone w jakiejkolwiek klasie końcowej, po co w ogóle je rozdzielać?
Rich Oliver,
56

Dodatkową różnicą jest to, że typy własne mogą określać typy nieklasowe. Na przykład

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

Typ własny jest tutaj typem strukturalnym. Efektem jest powiedzenie, że wszystko, co miesza się w Foo, musi implementować jednostkę zwracającą metodę „bez” argumentu „zamknij”. Pozwala to na bezpieczne miksy do pisania na kaczkach.

Dave Griffith
źródło
41
Właściwie możesz używać dziedziczenia także z typami strukturalnymi: klasa abstrakcyjna A rozszerza {def close: Unit}
Adrian
12
Myślę, że do pisania strukturalnego używa się refleksji, więc używaj go tylko wtedy, gdy nie ma innego wyboru ...
Eran Medan
@Adrian, uważam, że twój komentarz jest niepoprawny. `Klasa abstrakcyjna A rozszerza {def close: Unit}` jest po prostu klasą abstrakcyjną z nadklasą Object. to tylko dopuszczalna składnia Scali do bezsensownych wyrażeń. Możesz `klasa X rozszerza {def f = 1}; nowy X (). f` na przykład
Alexey,
1
@Alexey Nie rozumiem, dlaczego twój przykład (lub mój) jest bezsensowny.
Adrian
1
@Adrian, abstract class A extends {def close:Unit}jest równoważne z abstract class A {def close:Unit}. Nie dotyczy to typów konstrukcyjnych.
Alexey,
13

Sekcja 2.3 „Adnotacje o typie własnym” oryginalnego artykułu Scali Scalable Component Abstractions Martina Odersky'ego faktycznie bardzo dobrze wyjaśnia cel tego rodzaju poza kompozycją mixin: zapewnia alternatywny sposób kojarzenia klasy z typem abstrakcyjnym.

Przykład podany w artykule był następujący i nie wydaje się, aby miał elegancki korespondent podklasy:

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;
  }
}
lcn
źródło
Dla tych, którzy zastanawiają się, dlaczego podklasa nie rozwiąże tego problemu, sekcja 2.3 mówi również: „Każdy z operandów kompozycji mixinowej C_0 z… z C_n musi odnosić się do klasy. Mechanizm składu mixin nie pozwala żadnym C_i na odwołanie się do typu abstrakcyjnego. To ograniczenie umożliwia statyczne sprawdzanie niejednoznaczności i zastępowanie konfliktów w punkcie, w którym tworzona jest klasa. ”
Luke Maurer
12

Kolejna rzecz, o której nie wspomniano: ponieważ typy własne nie są częścią hierarchii wymaganej klasy, można je wykluczyć z dopasowywania wzorców, szczególnie gdy wyczerpuje się dopasowanie do zapieczętowanej hierarchii. Jest to wygodne, gdy chcesz modelować zachowania ortogonalne, takie jak:

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
Bruno Bieth
źródło
10

TL; DR streszczenie pozostałych odpowiedzi:

  • Rozszerzone typy są narażone na odziedziczone typy, ale typy własne nie

    np .: class Cow { this: FourStomachs }umożliwia stosowanie metod dostępnych tylko dla przeżuwaczy, takich jak digestGrass. Jednak cechy, które przedłużają Krowę, nie będą miały takich przywilejów. Z drugiej strony class Cow extends FourStomachsnarazi digestGrasskażdego, kto extends Cow .

  • samospecjalizacje pozwalają na cykliczne zależności, rozszerzanie innych typów nie

jazmit
źródło
9

Zacznijmy od zależności cyklicznej.

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

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

Jednak modułowość tego rozwiązania nie jest tak duża, jak mogłoby się wydawać na początku, ponieważ można zastąpić typy własne w następujący sposób:

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

Chociaż, jeśli zastąpisz członka typu własnego, utracisz dostęp do oryginalnego członka, do którego nadal można uzyskać dostęp poprzez super używanie dziedziczenia. Tak więc to, co naprawdę zyskuje się dzięki dziedziczeniu, to:

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

Teraz nie mogę twierdzić, że rozumiem wszystkie subtelności wzoru ciasta, ale uderza mnie to, że główną metodą egzekwowania modułowości jest kompozycja, a nie dziedziczenie lub samokształcenie.

Wersja dziedziczenia jest krótsza, ale głównym powodem, dla którego wolę dziedziczenie nad typami własnymi, jest to, że o wiele trudniej jest uzyskać prawidłową kolejność inicjowania z typami własnymi. Są jednak pewne rzeczy, które możesz zrobić z typami siebie, których nie możesz zrobić z dziedziczeniem. Typy własne mogą używać typu, podczas gdy dziedziczenie wymaga cechy lub klasy, jak w:

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

Możesz nawet zrobić:

trait TypeBuster
{ this: Int with String => }

Chociaż nigdy nie będziesz w stanie tego zrobić. Nie widzę żadnego bezwzględnego powodu, aby nie móc dziedziczyć po typie, ale z pewnością uważam, że użyteczne byłoby posiadanie klas i cech konstruktora ścieżki, ponieważ mamy cechy / klasy konstruktora typów. Jak niestety

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

Mamy to:

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 }

Albo to:

  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 }

Jednym z punktów, który należy bardziej wczuć w empatię, jest to, że cechy mogą rozszerzać klasy. Dziękujemy Davidowi Maclverowi za zwrócenie na to uwagi. Oto przykład z mojego własnego kodu:

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]

ScnBasedziedziczy z klasy Swing Frame, więc może być używany jako własny typ, a następnie dodawany na końcu (w instancji). Jednak val geomRmusi zostać zainicjowany, zanim zostanie wykorzystany przez dziedziczenie cech. Potrzebujemy więc klasy, aby wymusić wcześniejszą inicjalizację geomR. Klasa ScnVistamoże być następnie odziedziczona po wielu cechach ortogonalnych, z których można odziedziczyć. Korzystanie z wielu typów parametrów (generycznych) oferuje alternatywną formę modułowości.

Rich Oliver
źródło
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
}
Oleg Galako
źródło
4

Typ własny pozwala określić, jakie typy mogą mieszać cechę. Na przykład, jeśli masz cechę typu własnego Closeable, to ta cecha wie, że jedyne rzeczy, które można mieszać, muszą implementować Closeableinterfejs.

kikibobo
źródło
3
@ Blaisorblade: Zastanawiam się, czy mogłeś źle odczytać odpowiedź Kikibobo - typ własny cechy rzeczywiście pozwala ci ograniczyć typy, które mogą ją mieszać, i to jest część jej przydatności. Na przykład, jeśli zdefiniujemy, trait A { self:B => ... }to deklaracja X with Ajest ważna tylko wtedy, gdy X rozszerza B. Tak, możesz powiedzieć X with A with Q, gdzie Q nie rozszerza B, ale uważam, że kikibobo miał na myśli to, że X jest tak ograniczony. A może coś przeoczyłem?
AmigoNico,
1
Dzięki, masz rację. Mój głos został zablokowany, ale na szczęście mogłem edytować odpowiedź, a następnie zmienić swój głos.
Blaisorblade,
1

Aktualizacja: Zasadnicza różnica polega na tym, że typy własne mogą zależeć od wielu klas (przyznaję, że to trochę przypadek). Na przykład możesz mieć

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;
}

Pozwala to dodać Employeemixin tylko do wszystkiego, co jest podklasą Personi Expense. Oczywiście ma to znaczenie tylko wtedy, gdy Expenserozszerza się Personi na odwrót. Chodzi o to, że używanie typów własnych Employeemoże być niezależne od hierarchii klas, od których zależy. Nie dba o to, co się rozszerza - jeśli zmienisz hierarchię Expensevs Person, nie musisz modyfikować Employee.

Petr Pudlák
źródło
Pracownik nie musi być klasą, aby zejść z Osoby. Cechy mogą rozszerzać klasy. Jeśli cecha pracownika rozszerzyła osobę zamiast używać własnego typu, przykład nadal działałby. Uważam twój przykład za interesujący, ale nie wydaje się, aby ilustrował przypadek użycia dla typów własnych.
Morgan Creighton
@MorganCreighton Wystarczy, nie wiedziałem, że cechy mogą rozszerzać klasy. Zastanowię się, czy znajdę lepszy przykład.
Petr Pudlák
Tak, to zaskakująca funkcja językowa. Jeśli cecha Pracownik rozszerzyła klasę Osoba, to każda klasa ostatecznie „uschnięta” w Pracowniku również musiałaby rozszerzyć Osobę. Ale to ograniczenie jest nadal obecne, jeśli pracownik użył własnego typu zamiast rozszerzać osobę. Pozdrawiam Petr!
Morgan Creighton
1
Nie rozumiem, dlaczego „ma to sens tylko wtedy, gdy Wydatek rozszerza Osobę lub odwrotnie”.
Robin Green
0

w pierwszym przypadku pod cechę lub podklasę B można łączyć z dowolnymi zastosowaniami A. Zatem B może być cechą abstrakcyjną.

ItdayD
źródło
Nie, B może (i rzeczywiście jest) cechą abstrakcyjną w obu przypadkach. Więc nie ma różnicy z tej perspektywy.
Robin Green