393

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

4

8 回答 8

394

我能想到两个不同之处

  1. 抽象类可以具有构造函数参数以及类型参数。特征只能有类型参数。有一些讨论说,将来甚至 trait 也可以有构造函数参数
  2. 抽象类可以与 Java 完全互操作。您可以在没有任何包装器的情况下从 Java 代码中调用它们。只有在不包含任何实现代码的情况下,Traits 才能完全互操作
于 2010-01-02T11:53:57.400 回答
221

Scala 中的编程中有一节叫做“To trait, or not to trait?” 它解决了这个问题。由于第 1 版可以在线获取,我希望可以在这里引用整个内容。(任何认真的 Scala 程序员都应该买这本书):

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

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

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

如果要在 Java 代码中继承它,请使用抽象类。由于带有代码的特征没有与 Java 相似的类似物,因此从 Java 类中的特征继承往往很尴尬。同时,从 Scala 类继承与从 Java 类继承完全一样。作为一个例外,只有抽象成员的 Scala trait 会直接转换为 Java 接口,因此即使您希望 Java 代码继承自它,您也应该可以随意定义此类 trait。有关一起使用 Java 和 Scala 的更多信息,请参阅第 29 章。

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

如果效率很重要,则倾向于使用类。大多数 Java 运行时使类成员的虚拟方法调用比接口方法调用更快。特征被编译为接口,因此可能会产生轻微的性能开销。但是,只有在您知道所讨论的特征构成性能瓶颈并且有证据表明使用类实际上可以解决问题时,您才应该做出此选择。

如果您仍然不知道,在考虑了以上内容之后,请先将其作为一个特征。您以后可以随时更改它,并且通常使用特征可以打开更多选项。

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

另一个区别是super.

类和特征之间的另一个区别是,在类中,super调用是静态绑定的,而在特征中,它们是动态绑定的。如果你写super.toString在一个类中,你确切地知道将调用哪个方法实现。但是,当您在 trait 中编写相同的内容时,为 super 调用调用的方法实现在您定义 trait 时是未定义的。

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

编辑 1 (2013):

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

编辑 2(2018 年):

从 Scala 2.12 开始,trait 的二进制兼容性行为发生了变化。在 2.12 之前,向 trait 添加或删除成员需要重新编译继承该 trait 的所有类,即使这些类没有更改。这是由于 JVM 中对特征进行编码的方式。

从 Scala 2.12 开始,traits编译为 Java 接口,因此要求有所放宽。如果 trait 执行以下任何操作,其子类仍需要重新编译:

  • 定义字段(valvar,但常量是可以的 -final val没有结果类型)
  • 打电话super
  • 正文中的初始化语句
  • 扩展一个类
  • 依靠线性化在正确的超特征中找到实现

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

于 2013-03-11T02:31:26.233 回答
80

不管它值多少钱,Odersky 等人的Scala 编程建议,当你怀疑时,你使用特征。如果需要,您可以随时将它们更改为抽象类。

于 2010-01-02T14:45:44.363 回答
21

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

来自 Thomas在 Abstract Class 和 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
于 2014-04-08T12:59:30.070 回答
10

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

于 2010-01-02T09:23:36.000 回答
8

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

于 2012-09-23T10:22:34.643 回答
4

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

于 2010-01-02T09:34:43.563 回答
3
  1. 一个类可以继承多个特征,但只能继承一个抽象类。
  2. 抽象类可以具有构造函数参数以及类型参数。特征只能有类型参数。例如,你不能说 trait t(i: Int) { }; i 参数是非法的。
  3. 抽象类可以与 Java 完全互操作。您可以在没有任何包装器的情况下从 Java 代码中调用它们。只有在不包含任何实现代码的情况下,特征才能完全互操作。
于 2018-01-16T17:04:19.500 回答