自我类型和特征子类之间有什么区别?


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
Erik Kaplun 2014年

1
@Blaisorblade:这不是可以通过重新设计小语言解决的问题,而不是基本的限制吗?(至少从问题的角度来看)
Erik Kaplun 2014年

Answers:


273

它主要用于依赖注入,例如Cake模式。有一篇很棒的文章涵盖了Scala中许多不同形式的依赖项注入,包括Cake Pattern。如果您使用Google的“蛋糕图案和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使用a,但是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
谢谢。Cake模式是为什么我要谈论关于自我类型的炒作的90%...这是我第一次看到这个话题的地方。乔纳斯·博纳(Jonas Boner)的例子很好,因为它强调了我的问题。如果您将他的加热器示例中的自我类型更改为子特性,那会有什么区别(除了您定义正确的内容,而在定义ComponentRegistry时遇到的错误之外?
Dave

29
@戴夫:你的意思是像trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent?那将导致WarmerComponentImpl具有那些接口。他们将提供给任何扩展WarmerComponentImpl,这显然是错误的,因为它不是一个SensorDeviceComponent,也不是OnOffDeviceComponent。作为一个自我型,这些依赖关系都可以专门WarmerComponentImpl。A List可用作Array,反之亦然。但是它们只是不同的东西。
Daniel C. Sobral

10
谢谢丹尼尔。这可能是我要寻找的主要区别。实际的问题是,使用子类会将功能泄漏到您不想要的接口中。这是由于违反了特质的更为理论上的“一部分”规则的结果。自类型表示各部分之间的“用途”关系。
戴夫

11
@罗德尼不,不应该。实际上,this我很看重使用自我类型,因为它没有充分的理由掩盖原始的东西this
Daniel C. Sobral

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

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

在Odersky的书中,请参阅第33.5节(创建电子表格UI章节),其中提到:

在电子表格示例中,Model类继承自Evaluator,因此可以访问其评估方法。换句话说,Evaluator类将其自身类型定义为Model,如下所示:

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

希望这可以帮助。


3
我没有考虑过这种情况。这是我所见过的第一个示例,它与子类中的自类型不同。但是,这似乎有点边缘化,而且更重要的是,这似乎是个坏主意(我通常不尽一切努力不定义循环依赖项!)。您觉得这是最重要的区别吗?
戴夫

4
我认同。我没有其他理由为什么我更喜欢自类型而不是extends子句。自类型是冗长的,它们不会被继承(因此您必须将自类型作为一种仪式添加到所有子类型中),并且您只能看到成员但不能覆盖它们。我非常了解Cake模式,并且很多帖子都提到DI的自类型。但是我不知为何。我早在这里创建了一个示例应用程序(bitbucket.org/mushtaq/scala-di)。专门查看/ src / configs文件夹。我实现了DI,可以替换没有自类型的复杂Spring配置。
Mushtaq Ahmed

Mushtaq,我们同意。我认为Daniel关于不公开意外功能的声明很重要,但是正如您所说的,此“功能”有一个镜像视图……您不能覆盖该功能或在以后的子类中使用它。这很清楚地告诉我,设计何时会要求另一个。在发现真正需要之前,我将避免使用自类型设置-即,如Daniel指出的那样,如果我开始将对象用作模块。我使用隐式参数和简单的bootstrapper对象自动装配依赖项。我喜欢简单。
戴夫

@ DanielC.Sobral可能要感谢您的评论,但目前它的支持比您的支持者更多。都支持:)
rintcius 2012年

为什么不仅仅创建一个特征AB?由于特质A和特质B必须在任何最后的班级中都必须结合在一起,为什么要首先将它们分开?
Rich Oliver

56

另一个区别是自类型可以指定非类类型。例如

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

此处的自身类型是结构类型。效果是说,任何混在Foo中的东西都必须实现无参数的“关闭”方法返回单元。这样可以为鸭子输入提供安全的mixin。


41
实际上,您也可以将继承与结构类型一起使用:抽象类A扩展{def close:Unit}
Adrian

12
我认为结构化类型正在使用反射,因此仅在没有其他选择时才使用...
Eran Medan

@Adrian,我相信您的评论不正确。`抽象类A扩展{def close:Unit}`只是一个具有Object超类的抽象类。这只是Scala对无意义的表达式所允许的语法。您可以`X类扩展{def f = 1}; 例如新的X()。f`
Alexey,

1
@Alexey我不明白为什么您的例子(或我的例子)没有意义。
阿德里安

1
@Adrian abstract class A extends {def close:Unit}等同于abstract class A {def close:Unit}。因此,它不涉及结构类型。
Alexey'7

13

马丁·奥德斯基(Martin Odersky)最初的Scala论文“ 可伸缩组件抽象 ”的第2.3节“自类型注释” 实际上很好地解释了混合类型之外的自类型目的:提供了一种将类与抽象类型相关联的替代方法。

本文中给出的示例如下所示,并且似乎没有优雅的子类对应项:

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_n的mixin组合C_0的每个操作数都必须引用一个类。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

  • 自类型允许循环依赖,而扩展其他类型则不允许


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

现在,我不能声称理解蛋糕模式的所有微妙之处,但令我惊讶的是,执行模块化的主要方法是通过组合而不是继承或自身类型。

继承版本较短,但是我更喜欢继承而不是自己类型的主要原因是,我发现要正确设置自己类型的初始化顺序要困难得多。但是,可以使用自我类型执行某些操作,而不能使用继承进行某些操作。自我类型可以使用类型,而继承则需要特征或类,如:

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 }

应该更加理解的一点是,特质可以扩展类。感谢David Maclver指出这一点。这是我自己的代码中的一个示例:

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需要先进行初始化,然后再继承特征使用。因此,我们需要一个类来强制的初始化geomRScnVista然后,可以通过多个正交特性从这些类继承它们,而这些正交特性又可以从中继承。使用多个类型参数(泛型)提供了模块化的另一种形式。


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 type的特征Closeable,那么该特征就知道必须允许将其混入的唯一内容必须实现该Closeable接口。


3
@Blaisorblade:我想知道您是否可能误解了kibobobo的答案-特质的自我类型确实可以让您限制可能将其混入的类型,这就是其有用性的一部分。例如,如果我们定义,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;
}

这允许将Employeemixin仅添加到Personand 的子类中Expense。当然,这仅在Expense扩展时才有意义,Person反之亦然。关键是使用自类型Employee可以独立于它所依赖的类的层次结构。它不在乎是什么扩展-如果切换Expensevs 的层次结构Person,则无需修改Employee


Employee不必是从Person继承的类。性状可以扩展课堂。如果Employee特性扩展了Person而不是使用self类型,则该示例仍然有效。我发现您的示例很有趣,但是它似乎没有说明自我类型的用例。
Morgan Creighton 2012年

@MorganCreighton很公平,我不知道特质可以扩展课程。如果能找到更好的例子,我会考虑的。
彼得·普德拉克(PetrPudlák)2012年

是的,这是令人惊讶的语言功能。如果特征Employee扩展了Person类,那么最终“落入” Employee的任何类也必须扩展Person。但是,如果Employee使用自类型而不是扩展Person,则仍然存在该限制。干杯,彼得!
Morgan Creighton 2012年

1
我不明白为什么“这仅在费用扩展人,反之亦然时才有意义”。
罗宾·格林

0

在第一种情况下,可以将B的子特性或子类混入使用A的任何内容。因此B可以是抽象特性。


不,B在两种情况下都可以(并且确实是)“抽象特征”。因此从这个角度看没有什么区别。
罗宾·格林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.