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

421

一个用于特质A的自类型:

trait B
trait A { this: B => }

说的是:"A 不能被混合到没有扩展 B 的具体类中。"

另一方面,以下内容:

trait B
trait A extends B

它表明 "任何(具体或抽象)混合 A 的类也将混合 B"

这两个语句不是意思相同的吗?Self-type 似乎只是为了创建一种简单的编译时错误的可能性。

我错过了什么吗?


35
可以在自身类型中使用类型参数: trait A[Self] {this: Self => } 是合法的,trait A[Self] extends Self 不合法。 - Blaisorblade
4
自类型也可以是一个类,但特质不能继承自类。 - cvogt
10
一个特性可以从一个类中继承(至少在2.10版本中如此):http://pastebin.com/zShvr8LX - Erik Kaplun
1
@Blaisorblade:不过,这不是可以通过进行小型语言重新设计来解决的问题吗?(至少从问题的角度来看) - Erik Kaplun
我发现这个自类型非常有用,可以强制要求实现特质的类扩展一个(密封的)_trait-Enum_。请参阅此链接:http://stackoverflow.com/q/36066238/1206998 - Juh_
显示剩余3条评论
11个回答

295

它主要用于依赖注入,例如在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
                       ^

如果TweeterUser的子类,就不会出现错误。在上面的代码中,每当使用Tweeter时,我们都需要一个User,但是没有提供UserWrong,所以我们得到了一个错误。现在,在仍然在范围内的代码中,请考虑:
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 的要求。但是,上述第二个要求未得到满足:仍需为扩展 Right 的类/特质实现 User
使用 RightAgain,两个要求均已得到满足。提供了 UserUser 的实现。
对于更多实际应用场景,请参见本答案开头的链接!但是,希望现在你有所了解。

3
谢谢。蛋糕模式是我谈论自类型激动的原因之一,它是我第一次接触这个主题的地方。Jonas Boner的例子很好,因为它强调了我的问题的重点。如果您将他的加热器示例中的自类型更改为子特质,那么除了在定义ComponentRegistry时没有混入正确内容时出现的错误之外,还有什么区别吗? - Dave
30
@Dave: 你的意思是像trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent这样吗?这将会让WarmerComponentImpl拥有那些接口。它们会对任何扩展了WarmerComponentImpl的内容都是可用的,这显然是错误的,因为它_不是_SensorDeviceComponent或者OnOffDeviceComponent。作为自类型,这些依赖项仅对WarmerComponentImpl _独占_。一个List可以被用作一个Array,反之亦然。但它们并不是完全相同的东西。 - Daniel C. Sobral
12
谢谢 Daniel。这可能是我寻找的主要区别。实际问题在于使用子类化会将您不打算公开的功能泄露到接口中。这是违反了更理论的特质“是部分”的规则所导致的结果。自类型表达了部分之间的“使用”关系。 - Dave
11
不应该这样做。实际上,对于自身类型使用 this 是我不赞成的做法,因为它毫无意义地掩盖了原始的 this - Daniel C. Sobral
9
@opensas 尝试使用 self: Dep1 with Dep2 => - Daniel C. Sobral
显示剩余12条评论

164

自类型允许您定义循环依赖关系。例如,您可以实现以下内容:

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并因此获得了访问其evaluation方法的权限。要实现相反的效果,Evaluator类定义了其自身类型为Model,像这样:
package org.stairwaybook.scells
trait Evaluator { this: Model => ...

3
我之前没有考虑到这种情况。这是我见过的第一个不同于子类中的自身类型的例子。然而,这似乎是一种边缘情况,并且更重要的是,这似乎是一个不好的想法(我通常会尽力避免定义循环依赖关系!)。你认为这是最重要的区别吗? - Dave
4
我同意。我没有看到其他原因使我更喜欢自身类型而不是扩展子句。自身类型冗长,它们不会被继承(所以您必须将自身类型作为惯例添加到所有子类型中),并且您只能查看成员但不能重写它们。我很清楚 Cake 模式和许多帖子提到使用自身类型进行 DI。但是我还是不太确定。很久以前,我在这里创建了一个示例应用程序(http://bitbucket.org/mushtaq/scala-di/)请特别注意 /src/configs 文件夹。我使用 DI 实现了替换复杂的 Spring 配置,而没有使用自身类型。 - Mushtaq Ahmed
Mushtaq,我们意见一致。我认为Daniel关于不暴露无意功能的说法很重要,但正如你所说,这个“特性”有一个镜像视图...即你不能覆盖功能或在未来的子类中使用它。这很清楚地告诉我何时需要使用其中之一。除非我开始像Daniel指出的那样将对象用作模块,否则我将避免使用自身类型。我正在使用隐式参数和简单的引导程序对象进行自动装配依赖项。我喜欢这种简单性。 - Dave
@DanielC.Sobral 可能是因为你的评论,但目前它比你的回答获得了更多的赞同票。两个都点赞 :) - rintcius
为什么不创建一个特质AB呢?既然特质A和B必须在任何最终类中始终结合,为什么要一开始就将它们分开呢? - Rich Oliver
@RichOliver 我想答案是你无法控制宇宙中的每一个特性。 - Kevin Dreßler

62

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

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

这里的self类型是一个结构类型。作用是说任何混入Foo的内容都必须实现一个返回unit的无参“close”方法。这样可以安全地进行duck-typing混入。


44
实际上,您也可以在结构类型中使用继承:抽象类A扩展了{def close:Unit}。 - Adrian
12
我认为结构化类型是使用反射实现的,因此只有在没有其他选择时才使用它... - Eran Medan
@Adrian,我认为你的评论是不正确的。abstract class A extends {def close:Unit}只是一个抽象类,它的超类是Object。这只是Scala对无意义表达式的宽容语法。例如,您可以使用class X extends { def f = 1 }; new X().f - Alexey
1
@Alexey 我不明白为什么你的例子(或者我的)是毫无意义的。 - Adrian
1
@Adrian,“abstract class A extends {def close:Unit}”等同于“abstract class A {def close:Unit}”,因此不涉及结构类型。 - Alexey

17

还有一件事情没有提到:由于自类型不是所需类的层次结构的一部分,因此它们可以从模式匹配中排除,特别是当您在穷尽地匹配封闭层次结构时。当您想要建模正交行为时,这非常方便,例如:

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

13

马丁·奥德斯基原始的Scala论文可扩展组件抽象中的第2.3节“Selftype Annotations”实际上很好地解释了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引用抽象类型。这个限制使得在组合类的点上可以静态地检查歧义和覆盖冲突。” - Luke Maurer

13

其他答案的简要概述:

  • 你扩展的类型将对继承的类型公开,但self-types则不会

    例如:class Cow { this: FourStomachs }允许您使用仅适用于反刍动物(例如digestGrass)的方法。 然而,扩展Cow的Trait将没有这样的特权。 另一方面,class Cow extends FourStomachs将把digestGrass暴露给任何extends Cow的人。

  • self-types允许循环依赖关系,扩展其他类型则不允许


10

让我们从循环依赖开始。

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。因此,我们需要一个类来强制先初始化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
自类型可以让你指定哪些类型可以混入一个特质。例如,如果你有一个带有自类型Closeable的特质,那么该特质知道只有实现了Closeable接口的东西才能混入它。

3
我想知道你是否可能误读了kikibobo的回答——特质的自身类型确实允许您约束可能混合它的类型,并且这是其有用性的一部分。例如,如果我们定义trait A { self:B => ... },那么声明X with A只有在X扩展B时才有效。是的,您可以说X with A with Q,其中Q不扩展B,但我相信kikibobo的观点是X受到如此限制。或者我错过了什么? - AmigoNico
1
谢谢,你说得对。我的投票已锁定,但幸运的是我能够编辑答案然后改变我的投票。 - Blaisorblade

2

更新:一个主要的区别是自类型可以依赖于多个类(我承认这有点边角案例)。例如,你可以有

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 mixin 添加到任何PersonExpense的子类中。当然,只有在Expense扩展Person或反之亦然时,这才有意义。关键是使用自类型,Employee可以独立于其所依赖的类的层次结构。它不关心什么继承了什么 - 如果您切换ExpensePerson的层次结构,则不必修改Employee

员工不需要从人类继承,特质可以扩展类。如果Employee特质扩展了Person而不是使用自身类型,示例仍将有效。我认为你的例子很有趣,但似乎并没有说明自身类型的用例。 - Morgan Creighton
@MorganCreighton 好的,我不知道特质可以扩展类。如果我能找到更好的例子,我会考虑它的。 - Petr
1
是的,这是一个令人惊讶的语言特性。如果 trait Employee 扩展了类 Person,那么最终“withed in” Employee 的任何类也必须扩展 Person。但是,如果 Employee 使用自身类型而不是扩展 Person,则该限制仍然存在。干杯,Petr! - Morgan Creighton
2
我不明白为什么“这只有在Expense扩展Person或反之亦然时才有意义。” - Robin Green

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接