Öz-tipler ve sürekli alt sınıflar arasındaki fark nedir?


387

Bir özellik için kendi kendine tip A:

trait B
trait A { this: B => }

diyor " Ada uzanmayan bir beton sınıfına arada kullanılamaz B" .

Öte yandan, aşağıdakiler:

trait B
trait A extends B

"herhangi bir (somut veya soyut) sınıf karışımının Ada B'de karıştırılacağını " belirtir .

Bu iki ifade aynı anlama gelmiyor mu? Kendiliğinden tip, sadece basit bir derleme zamanı hatası olasılığını yaratmak için kullanılır.

Neyi kaçırıyorum?


Aslında burada benlik tipleri ve özelliklerde alt sınıflar arasındaki farklarla ilgileniyorum. Benlik türleri için bazı ortak kullanımları biliyorum; Alt tipleme ile aynı şekilde daha net yapılmaması için bir neden bulamıyorum.
Dave

32
Kişi kendi kendine türlerde tür parametrelerini kullanabilir: trait A[Self] {this: Self => }yasaldır, trait A[Self] extends Selfdeğil.
Blaisorblade

3
Kendi kendine tip de bir sınıf olabilir, ancak özellik sınıftan miras alınamaz.
cvogt

10
@cvogt: bir özellik bir sınıftan miras alabilir (en az 2.10 itibariyle): pastebin.com/zShvr8LX
Erik Kaplun

1
@Blaisorblade: Bu, küçük bir dilin yeniden tasarlanmasıyla çözülebilecek bir şey değil mi, temel bir sınırlama değil mi? (en azından sorunun bakış açısından)
Erik Kaplun

Yanıtlar:


273

Çoğunlukla Kek Kalıbı gibi Bağımlılık Enjeksiyonu için kullanılır . Scala'da Kek Kalıbı da dahil olmak üzere birçok farklı bağımlılık enjeksiyonunu kapsayan harika bir makale var . Google "Kek Kalıbı ve Scala" iseniz, sunumlar ve videolar da dahil olmak üzere birçok bağlantı alırsınız. Şimdilik başka bir soruya bağlantı .

Şimdi, kendi kendine bir tür ile bir özelliği genişletme arasındaki farkın ne olduğu konusunda, bu basittir. Eğer derseniz B extends A, o B zaman bir A. Eğer kendi kendine türlerini kullanırken, B gerektirir bir A. Öz türlerle oluşturulan iki özel gereksinim vardır:

  1. Eğer Buzatılır, o zaman konum gerekli karıştırmak-in bir A.
  2. Somut bir sınıf nihayet bu özellikleri genişlettiğinde / karıştığında, bazı sınıf / özelliklerin uygulanması gerekir A.

Aşağıdaki örnekleri düşünün:

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
                       ^

Eğer Tweeterbir alt sınıf Userolsaydı, hata olmazdı. Yukarıdaki kodda, biz gerekli bir Userzaman Tweeterkullanılır ancak, Usersağlanan değildi Wrongbiz bir hata var bu yüzden. Şimdi, yukarıdaki kod hala kapsamdayken şunları göz önünde bulundurun:

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

İle Rightkarıştırmak için gereksinim Userkarşılanır. Bununla birlikte, yukarıda belirtilen ikinci gereklilik karşılanmamıştır: uygulama yükü, Userdevam eden sınıflar / özellikler için hala devam etmektedir Right.

İle RightAgainher iki gereksinimleri karşılanır. A Userve bir uygulaması Usersağlanmıştır.

Daha pratik kullanım örnekleri için lütfen bu cevabın başındaki bağlantılara bakınız! Ama, umarım şimdi anlarsın.


3
Teşekkürler. Kek kalıbı, kendinden türler hakkındaki hype hakkında neden bahsettiğimin% 90'ı ... Konuyu ilk gördüğüm yer. Jonas Boner'in örneği harika çünkü sorumun altını çiziyor. Isıtıcı örneğindeki kendi tiplerini alt sınırlar olarak değiştirirseniz, fark ne olurdu (doğru şeyleri karıştırmazsanız ComponentRegistry'i tanımlarken aldığınız hata dışında?)
Dave

29
@Dave: Yani trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponentmi demek istiyorsun? Bu WarmerComponentImplarayüzlere sahip olur . Onlar genişletilmiş bir şey için kullanılabilir olacaktır WarmerComponentImplolduğu gibi, açıkça yanlış, değil bir SensorDeviceComponent, ne de OnOffDeviceComponent. Bir öz türü olarak, bu bağımlılıklar mevcuttur münhasıran için WarmerComponentImpl. A , bir ve tam tersi Listolarak kullanılabilir Array. Ama aynı şey değiller.
Daniel C.Sobral

10
Teşekkürler Daniel. Bu muhtemelen aradığım en büyük ayrım. Pratik sorun, alt sınıf kullanmanın işlevselliği arayüzünüzde istemediğiniz şekilde sızdırmasıdır. Bu, özellikler için daha teorik "bir kısmı" kuralının ihlali sonucudur. Öz-tipler, parçalar arasındaki "kullanım-a" ilişkisini ifade eder.
Dave

11
@Rodney Hayır, olmamalı. Aslında, thiskendi türleriyle kullanmak aşağıya baktığım bir şeydir, çünkü orijinalin iyi bir nedeni yok this.
Daniel C.Sobral

9
@opensas Deneyin self: Dep1 with Dep2 =>.
Daniel C.Sobral

156

Kendini türleri döngüsel bağımlılıkları tanımlamanızı sağlar. Örneğin, bunu başarabilirsiniz:

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

Kullanarak miras extendsbuna izin vermez. Deneyin:

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

Odersky kitabında, bahsettiği bölüm 33.5'e (Elektronik tablo kullanıcı arayüzü oluşturma) bakın:

Elektronik tablo örneğinde, sınıf Modeli Değerlendiriciden miras alır ve böylece değerlendirme yöntemine erişim kazanır. Diğer taraftan, sınıf Değerlendiricisi kendi türünü Model olarak tanımlar, şöyle:

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

Bu yardımcı olur umarım.


3
Bu senaryoyu düşünmemiştim. Gördüğüm bir şeyin ilk örneği, bir alt sınıfta olduğu gibi kendi kendine bir türle aynı değildir. Ancak, bir tür kenar-casey gibi görünüyor ve daha da önemlisi, kötü bir fikir gibi görünüyor (genellikle döngüsel bağımlılıkları tanımlamak DEĞİL yolumdan çok uzağa gidiyorum!). Bunun en önemli ayrım olduğunu düşünüyor musunuz?
Dave

4
Sanırım. Benlik türlerini maddeyi genişletmek için tercih etmemin başka bir nedeni görmüyorum. Kendiliğinden türler ayrıntılıdır, devralınmazlar (bu nedenle tüm alt türlere ritüel olarak kendiliğinden türler eklemeniz gerekir) ve yalnızca üyeyi görebilir, ancak bunları geçersiz kılamazsınız. Ben Kek desen ve DI için öz-tipler söz birçok yazı farkındayım. Ama bir şekilde ikna olmadım. Burada uzun zamandır örnek bir uygulama oluşturmuştum ( bitbucket.org/mushtaq/scala-di ). Özellikle / src / configs klasörüne bakın. Kendine özgü olmayan karmaşık Spring konfigürasyonlarının yerine DI elde ettim.
Mushtaq Ahmed

Mushtaq, hemfikiriz. Bence Daniel'in kasıtsız işlevselliği açığa vurmama konusundaki ifadesi önemli bir şeydir, ancak koyduğunuzda bu 'özelliğin' bir ayna görünümü vardır ... işlevselliği geçersiz kılamaz veya gelecekteki alt sınıflarda kullanamazsınız. Bu bana açıkça tasarımın ne zaman diğerinin üstesinden geleceğini söyler. Gerçek bir ihtiyaç bulana kadar benlik türlerinden kaçınacağım. Ben örtük parametreleri ve basit bir bootstrapper nesnesi ile otomatik kablo bağımlılıkları değilim. Basitliği seviyorum.
Dave

@ DanielC.Sobral yorumunuz sayesinde olabilir, ancak şu anda anser'ınızdan daha fazla oy veriyor. Her ikisi de upvoting :)
rintcius

Neden sadece bir özellik AB oluşturmuyorsunuz? A ve B özellikleri her zaman herhangi bir son sınıfta birleştirilmelidir, neden ilk etapta onları ayırmalısınız?
Rich Oliver

56

Diğer bir fark, kendi tiplerinin sınıf dışı tipler belirleyebilmeleridir. Örneğin

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

Buradaki öz tip yapısal bir tiptir. Etkisi, Foo'da karışan her şeyin arg-no "kapat" yöntemi döndüren bir birim uygulaması gerektiğini söylemek. Bu, ördek yazma için güvenli karışımlara izin verir.


41
Aslında kalıtsal yapı türleriyle de kullanabilirsiniz: soyut A sınıfı uzanır {def close: Unit}
Adrian

12
Bence yapısal yazım yansıma kullanıyor, bu yüzden sadece başka seçenek olmadığında kullan ...
Eran Medan

@Adrian, yorumunun yanlış olduğuna inanıyorum. `soyut sınıf A, {def close: Unit}` yi genişletir, yalnızca Object üst sınıfına sahip soyut bir sınıftır. bu sadece bir Scala'nın saçma ifadelere izin veren sözdizimidir. `X sınıfı genişletmeler {def f = 1}; yeni X (). f` örneğin
Alexey

1
@Alexey Örneğinizin (ya da benim) neden saçma olduğunu anlamıyorum.
Adrian

1
@Adrian, abstract class A extends {def close:Unit}eşdeğerdir abstract class A {def close:Unit}. Dolayısıyla yapısal türleri içermez.
Alexey

13

Bölüm 2.3 Martin Odersky'nin orijinal Scala makalesi Ölçeklendirilebilir Bileşen Soyutlamalarının "Selftype Ek Açıklamaları" aslında selfin türünün mixin kompozisyonunun ötesindeki amacını çok iyi açıklamaktadır: bir sınıfı soyut bir türle ilişkilendirmenin alternatif bir yolunu sağlamak.

Makalede verilen örnek aşağıdaki gibiydi ve zarif bir alt sınıf muhabirine sahip görünmüyor:

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

Alt sınıflamanın bunu neden çözemediğini merak edenler için, Bölüm 2.3 ayrıca şunları da söylüyor: “C_0 ile C_0 olan bir mixin kompozisyonunun işlenenlerinin her biri bir sınıfa başvurmalıdır. Karışım bileşim mekanizması, herhangi bir C_i'nin soyut bir türe başvurmasına izin vermez. Bu kısıtlama, bir sınıfın oluştuğu noktada belirsizlikleri statik olarak kontrol etmeyi ve çakışmaları geçersiz kılmayı mümkün kılıyor. ”
Luke Maurer

12

Bahsedilmeyen başka bir şey: öz-tipler gerekli sınıfın hiyerarşisinin bir parçası olmadığından, özellikle kapalı bir hiyerarşiye karşı tam olarak eşleştiğinizde, desen eşleşmesinden hariç tutulabilirler. Bu, aşağıdaki gibi dikey davranışları modellemek istediğinizde kullanışlıdır:

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; diğer cevapların DR özeti:

  • Genişlettiğiniz türler kalıtsal türlere maruz kalır, ancak öz türler

    örneğin: class Cow { this: FourStomachs }yalnızca geviş getiren hayvanların kullanabileceği yöntemleri kullanmanızı sağlar digestGrass. Bununla birlikte, İnek'i genişleten özelliklerin böyle bir ayrıcalığı olmayacaktır. Öte yandan, herkese açık class Cow extends FourStomachsolacaktır .digestGrassextends Cow

  • kendiliğinden türler döngüsel bağımlılıklara izin verir, diğer türleri genişletmez


9

Döngüsel bağımlılıkla başlayalım.

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

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

Ancak, bu çözümün modülerliği ilk göründüğü kadar büyük değildir, çünkü kendiliğinden türleri geçersiz kılabilirsiniz:

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

Kendi türünde bir üyeyi geçersiz kılarsanız, orijinal üyeye erişiminizi kaybedersiniz, buna yine de miras kullanarak süper erişim yoluyla erişebilirsiniz. Yani kalıtım kullanarak gerçekten kazanılan şey:

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

Şimdi kek kalıbının tüm inceliklerini anlayabildiğimi iddia edemem, ancak modülerliği güçlendirmenin ana yönteminin miras veya kendinden türlerden ziyade kompozisyon yoluyla olması bana çarpıcı geliyor.

Kalıtım sürümü daha kısadır, ancak kendi türlerine göre kalıtımı tercih etmemin temel nedeni, kendi kendine türlerle başlatma sırasını doğru almanın çok daha zor olduğunu görmemdir. Ancak, kalıtımla yapamayacağınız öz türlerle yapabileceğiniz bazı şeyler vardır. Kendi kendine türler bir tür kullanabilirken, kalıtım aşağıdaki gibi bir özellik veya sınıf gerektirir:

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

Hatta şunları yapabilirsiniz:

trait TypeBuster
{ this: Int with String => }

Buna rağmen asla başaramayacaksınız. Bir türden miras kalmamak için kesin bir neden görmüyorum, ancak kesinlikle tür yapıcı sınıfları / sınıflarımız olduğu için yol yapıcı sınıfları ve özelliklerine sahip olmanın yararlı olacağını hissediyorum. Ne yazık ki

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

Bizde bu var:

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 }

Veya bu:

  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 }

Daha fazla empati edilmesi gereken bir nokta, özelliklerin sınıfları genişletebilmesidir. David Maclver'e bunu işaret ettiği için teşekkürler. İşte kendi kodumdan bir örnek:

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]

ScnBaseSwing Frame sınıfından miras alır , bu nedenle kendi kendine bir tür olarak kullanılabilir ve daha sonra sonunda karıştırılabilir (örnekleme sırasında). Ancak, val geomRözellikler devralınarak kullanılmadan önce başlatılması gerekir. Bu yüzden önceden başlatılmasını sağlamak için bir sınıfa ihtiyacımız var geomR. Sınıf ScnVistadaha sonra kendileri miras alınabilecek birden çok dikey özellikten miras alınabilir. Çoklu tip parametreleri (jenerikler) kullanmak alternatif bir modülerlik biçimi sunar.


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

Kendi kendine bir tür, bir özellikte hangi türlerin karışmasına izin verileceğini belirlemenizi sağlar. Örneğin, kendi türünde bir özelliğiniz varsa Closeable, bu özellik, onu karıştırmasına izin verilen tek şeyin Closeablearabirimi uygulaması gerektiğini bilir .


3
@Blaisorblade: Kikibobo'nun cevabını yanlış okuyabileceğinizi merak ediyorum - bir özelliğin kendi türü gerçekten de onu karıştırabilecek türleri kısıtlamanıza izin veriyor ve bu yararlılığının bir parçası. Örneğin, trait A { self:B => ... }bir açıklama X with Ayalnızca X'in B'yi uzatması durumunda geçerlidir. Evet, X with A with QQ'nun B'yi genişletmediği diyebilirsiniz , ancak kikibobo'nun X'in bu kadar kısıtlanmış olduğuna inanıyorum. Yoksa bir şey mi kaçırdım?
AmigoNico

1
Teşekkürler, haklısın. Oyum kilitlendi, ama neyse ki yanıtı düzenleyip oyumu değiştirebilirim.
Blaisorblade

1

Güncelleme: Temel fark, kendi tiplerinin birden fazla sınıfa bağlı olabileceğidir (bu biraz köşe durum olduğunu itiraf ediyorum). Örneğin,

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

Bu, Employeemixin'i Personve alt sınıfı olan herhangi bir şeye eklemenizi sağlar Expense. Tabi ki, bu anlamlıdır Expenseuzanır Personya da tam tersi. Mesele şu ki, öz-tipler kullanmak Employeebağımlı olduğu sınıfların hiyerarşisinden bağımsız olabilir. Neyin neyi genişlettiği umurumda değil - Expensevs hiyerarşisini Persondeğiştirirseniz, değiştirmeniz gerekmez Employee.


Çalışanın Kişiden inmek için bir sınıf olması gerekmez. Özellikler sınıfları genişletebilir. Çalışan özelliği, kendi türünü kullanmak yerine Kişiyi genişlettiğinde, örnek yine de işe yarayacaktır. Örneğinizi ilginç buluyorum, ancak kendi kendine türler için bir kullanım örneği göstermiyor gibi görünüyor.
Morgan Creighton

@MorganCreighton Fair yeterince, özelliklerin sınıfları genişletebileceğini bilmiyordum. Daha iyi bir örnek bulabilirsem düşüneceğim.
Petr Pudlák

Evet, şaşırtıcı bir dil özelliği. Eğer çalışan Çalışan sınıf Kişisini genişletirse, o zaman sonuçta "solmuş" ne olursa olsun "Çalışan da Kişiyi genişletmek zorunda kalacaktır. Ancak, Çalışan Kişiyi genişletmek yerine kendi kendine bir tür kullanıyorsa, bu kısıtlama hala mevcuttur. Şerefe Petr!
Morgan Creighton

1
"Bu sadece Gider Kişiyi uzatırsa ya da tam tersi olursa anlamlıdır" anlamıyorum.
Robin Green

0

ilk durumda, B'nin bir alt-özelliği veya alt-sınıfı, A'nın kullandığı her şeye karışabilir. Dolayısıyla B, soyut bir özellik olabilir.


Hayır, B her iki durumda da "soyut bir özellik" olabilir (ve aslında). Yani bu açıdan hiçbir fark yok.
Robin Green
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.