使用抽象类而不是特征的优点是什么?


371

使用抽象类而不是特征(除了性能)有什么好处?在大多数情况下,抽象类似乎可以用特征代替。

Answers:


371

我可以想到两个差异

  1. 抽象类可以具有构造函数参数以及类型参数。特性只能具有类型参数。有人讨论过,即使将来特征也可以具有构造函数参数
  2. 抽象类可与Java完全互操作。您可以从Java代码中调用它们而无需任何包装。特性仅在不包含任何实现代码的情况下才可以完全互操作

172
非常重要的附录:一个类可以继承多个特征,但只能继承一个抽象类。我认为这应该是开发人员在考虑在几乎所有情况下使用哪个时提出的第一个问题。
2015年

15
救生员:“特质只有在不包含任何实现代码的情况下,才能完全互操作”
Walrus the Cat

2
抽象-当集体行为定义或导致一个对象(对象的分支)但仍不构成(准备)对象时。特质,当您需要引入功能时,即功能永远不会源于对象的创建,当对象从隔离中出来并必须进行通信时,它就会演变或需要。
拉米兹·乌丁

5
想想,第二个区别在Java8中不存在。
阮阮

14
根据Scala 2.12,特征可以编译为Java 8接口-scala-lang.org/news/2.12.0#traits-compile-to-interfaces
凯文·梅雷迪斯

209

Scala编程中有一节称为“是特质还是不特质?”。解决了这个问题。由于第一版可在线获得,我希望可以在这里引用整个内容。(任何认真的Scala程序员都应该买这本书):

每当实现行为的可重用集合时,都必须决定要使用特征还是抽象类。没有严格的规则,但是本节包含一些要考虑的准则。

如果该行为不会被重用,则将其设为一个具体的类。毕竟这不是可重用的行为。

如果可以在多个不相关的类中重用它,请使其成为特征。只有特征可以混入类层次结构的不同部分。

如果要在Java代码中从中继承,请使用抽象类。由于具有代码的特征没有紧密的Java类似物,因此从Java类中的特征继承往往很尴尬。同时,从Scala类继承就像从Java类继承一样。一个例外是,仅具有抽象成员的Scala特性直接转换为Java接口,因此即使您期望Java代码从其继承,您也可以随意定义此类特性。有关一起使用Java和Scala的更多信息,请参见第29章。

如果打算以编译形式分发它,并且希望外部团体编写从其继承的类,则您可能倾向于使用抽象类。问题是,当一个特性获得或失去一个成员时,任何从其继承的类都必须重新编译,即使它们没有更改。如果外部客户只会调用行为,而不是继承行为,那么使用特征就可以了。

如果效率非常重要,则倾向于使用课程。大多数Java运行时使对类成员的虚拟方法调用比接口方法调用更快。特性被编译到接口,因此可能会付出一点性能开销。但是,仅当您知道所讨论的特征构成性能瓶颈并有证据证明使用类实际上可以解决问题时,才应做出此选择。

如果您仍然不知道,请在考虑了上述内容之后,将其作为特征开始。您以后总是可以更改它,通常使用特征可以使更多选项保持打开状态。

正如@Mushtaq Ahmed所提到的,特征不能具有任何传递给类的主构造函数的参数。

另一个区别是治疗super

类和特性之间的另一个区别是,在类中,super调用是静态绑定的,而在特性中,它们是动态绑定的。如果您super.toString在类中编写代码,那么您将确切知道将调用哪种方法实现。但是,当您在特征中编写相同内容时,定义特征时,用于超级调用的方法实现是不确定的。

有关更多详细信息,请参见第12章的其余部分。

编辑1(2013):

与特征相比,抽象类的行为方式存在细微差异。线性化规则之一是,它保留了类的继承层次结构,这倾向于将抽象类推到链的后面,而特征可以很容易地混合进来。在某些情况下,实际上最好放在类线性化的后一个位置,因此可以使用抽象类。请参阅Scala中的约束类线性化(混合顺序)

编辑2(2018):

从Scala 2.12开始,特征的二进制兼容性行为已更改。在2.12之前,向特性添加或删除成员都需要重新编译所有继承特性的类,即使这些类未更改也是如此。这是由于在JVM中对特征进行编码的方式所致。

从Scala 2.12开始,特征可以编译到Java接口,因此要求有所放松。如果特征执行以下任何一项操作,则其子类仍需要重新编译:

  • 定义字段(valvar,但是常量可以- final val没有结果类型)
  • 呼唤 super
  • 主体中的初始化器语句
  • 扩展课程
  • 依靠线性化在正确的特征中找到实现

但是,如果特征不具备,您现在可以在不破坏二进制兼容性的情况下对其进行更新。


2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine-有人可以解释一下有什么区别吗?extendswith
2015年

2
@ 0fnt他的区别与扩展与否无关。他的意思是,如果仅在同一编译中混用特征,则二进制兼容性问题不适用。但是,如果您的API设计为允许用户自己混入特征,那么您将不得不担心二进制兼容性。
John Colanduoni 2015年

2
@ 0fnt:extends和之间绝对没有语义差异with。它纯粹是语法上的。如果您从多个模板继承,则第一个获取extend,其他所有获取with,就是这样。认为with是逗号:class Foo extends Bar, Baz, Qux
约尔格W¯¯米塔格


20

除了您不能直接扩展多个抽象类,而是可以将多个特征混入一个类之外,值得一提的是,特征是可堆叠的,因为特征中的超级调用是动态绑定的(它是指在混合之前混合了一个类或特征当前一个)。

从托马斯关于抽象类与特质的区别的回答

trait A{
    def a = 1
}

trait X extends A{
    override def a = {
        println("X")
        super.a
    }
}  


trait Y extends A{
    override def a = {
        println("Y")
        super.a
    }
}

scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1

scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1

9

扩展抽象类时,这表明子类是类似的类型。我认为使用特质不一定是这种情况。


这是否有任何实际含义,还是只会使代码更易于理解?
拉尔夫(Ralf)2010年

8

作者在《Programming Scala》中说,抽象类建立了经典的面向对象的“ is-a”关系,而特征则是构成的一种scala方式。


5

抽象类可以包含行为-它们可以使用构造函数args(其特性无法实现)进行参数化,并表示一个工作实体。特质只代表一个功能,一个功能的接口。


8
希望您不是在暗示特质不能包含行为。两者都可以包含实现代码。
米奇·布莱文斯

1
@Mitch Blevins:当然不是。它们可以包含代码,但是当您定义trait Enumerable许多辅助函数时,我不会将它们称为行为而只是将其与一项功能相关联。
达里奥

4
@Dario我将“行为”和“功能”视为同义词,因此我发现您的答案非常混乱。
David J.

3
  1. 一个类可以继承多个特征,但只能继承一个抽象类。
  2. 抽象类可以具有构造函数参数以及类型参数。特性只能具有类型参数。例如,您不能说特征t(i:Int){}; i参数是非法的。
  3. 抽象类可与Java完全互操作。您可以从Java代码中调用它们而无需任何包装。特性只有在不包含任何实现代码的情况下才可以完全互操作。
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.