使用抽象类与使用特质相比有什么优势?

405

除了性能方面,使用抽象类的优点是什么?似乎大多数情况下抽象类都可以被 trait 取代。

8个回答

402

我能想到两个区别

  1. 抽象类可以有构造函数参数和类型参数,而特质只能有类型参数。未来可能会讨论使特质也能拥有构造函数参数
  2. 抽象类与Java完全互操作。您可以在没有任何包装器的情况下从Java代码调用它们。仅当特质不包含任何实现代码时,它们才是完全互操作的。

189
非常重要的附注:一个类可以从多个 trait 继承,但只能继承一个抽象类。我认为这应该是开发者在考虑使用哪种方式时几乎所有情况下首先要问的问题。 - BAR
19
“Traits只有在不包含任何实现代码的情况下才能完全互操作。” - Walrus the Cat
2
抽象 - 当集体行为定义或导致一个对象(对象的分支),但仍未组合成(准备好的)对象时。特征,当您需要引入能力,即能力不是随着对象的创建而产生的,它是在对象从孤立状态中出来并需要通信时演变或需要的。 - Ramiz Uddin
6
Java8中不存在第二个区别,请思考。 - Duong Nguyen
14
根据Scala 2.12的规定,特质会被编译成Java 8接口 - http://scala-lang.org/news/2.12.0#traits-compile-to-interfaces。 - Kevin Meredith
在Scala中这是什么意思:Trait可以被添加到对象实例中。 抽象类不能被添加到对象实例中。 - Shasu

226

《Scala编程》一书中有一个章节叫做“使用Trait还是不使用Trait?”"(To trait, or not to trait?)", 专门讨论这个问题。由于第一版可以在线获得,我希望引用整篇文章不会有问题(任何严肃的Scala程序员都应该购买这本书):

无论何时你实现一个可重用的行为集合,你都需要决定是要使用trait还是抽象类。没有确定的规则,但本节包含了几个考虑因素。
如果行为不会被重复使用,则将其制作成具体类。毕竟,它不是可重用的行为。
如果行为可能在多个不相关的类中被重复使用,则将其制作成trait。只有trait可以混入到类层次结构的不同部分中。
如果想要在Java代码中继承它,请使用抽象类。由于带有代码的trait没有近似的Java类比,所以在Java类中继承trait往往很笨拙。而从Scala类继承和从Java类继承完全相同。然而,作为一种例外,仅具有抽象成员的Scala trait直接转换为Java接口,因此即使您希望Java代码继承它,也可以定义此类trait。有关在Java和Scala之间工作的更多信息,请参见第29章。
如果计划以编译形式分发它,并且您期望外部组写类来继承它,则可能倾向于使用抽象类。问题在于,当trait获得或失去成员时,任何继承自它的类都必须重新编译,即使它们没有更改。如果外部客户端仅调用行为,而不是继承它,则使用trait是可以的。
如果效率非常重要,请倾向于使用类。大多数Java运行时使对类成员的虚拟方法调用比接口方法调用更快。Trait编译为接口,因此可能会付出轻微的性能开销。但是,只有在你知道所涉及的trait构成了性能瓶颈,并且有证据表明使用类实际上解决了问题时,才应该做出这个选择。
如果你仍然不知道,在考虑了以上因素之后,那就先把它作为一个trait制作。您随时可以更改它,通常使用trait可以保持更多的选项。
如@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
如果外部客户端只会调用行为,而不是继承它,那么使用 trait 是可以的。有人能解释一下这里的区别吗?extendswith 有什么区别? - 0fnt
2
他的区别并不在于extends vs with。他的意思是,如果你只在同一编译中混合trait,则二进制兼容性问题不适用。然而,如果您的API旨在允许用户自己混合trait,则必须担心二进制兼容性问题。 - John Colanduoni
2
@0fnt:extendswith之间绝对没有语义上的区别,它们纯粹是语法上的。如果你从多个模板中继承,第一个使用extend,其余的都使用with,就是这样。把with看作逗号:class Foo extends Bar, Baz, Qux - Jörg W Mittag
在Scala中这是什么意思:Trait可以被添加到对象实例中。 抽象类不能被添加到对象实例中。 - Shasu

80

21

除了你不能直接扩展多个抽象类之外,但你可以将多个traits混合到一个类中。值得一提的是,traits是可堆叠的,因为trait中的super调用是动态绑定的(它是指向在当前trait之前被混合的类或trait)。

来自Thomas在Difference between Abstract Class and Trait中的回答:

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

10

在扩展抽象类时,这表明子类是类似类型的。但使用特性并非必然如此。


这是否有任何实际影响,还是只是使代码更易于理解? - Ralf

8
Programming Scala中,作者说抽象类建立了经典的面向对象“is-a”关系,而特质是Scala语言中一种组合的方式。

4

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


9
希望您并未意味着特征无法包含行为。两者都可以包含实现代码。 - Mitch Blevins
1
@Mitch Blevins:当然不是。它们可以包含代码,但当你使用 trait Enumerable 定义了许多帮助函数时,我不会称它们为行为,而只是与一个功能相关的功能。 - Dario
5
我认为“行为”和“功能”是同义词,因此我觉得你的回答很令人困惑。 - David J.

4
  1. 一个类可以继承多个特质(trait),但只能继承一个抽象类(abstract class)。
  2. 抽象类可以有构造器参数和类型参数,而特质只能有类型参数。例如,你不能写出 trait t(i: Int) { },因为 i 参数是非法的。
  3. 抽象类可以与Java完全互操作。你可以在Java代码中直接调用它们,无需任何包装器(wrapper)。而特质只有当它们不包含任何实现代码时才能完全互操作。

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