ChatGPT解决这个技术问题 Extra ChatGPT

使用抽象类而不是特征有什么好处?

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


M
Mushtaq Ahmed

我能想到两个不同之处

抽象类可以具有构造函数参数以及类型参数。特征只能有类型参数。有一些讨论认为,将来甚至 trait 也可以具有构造函数参数 抽象类可以与 Java 完全互操作。您可以在没有任何包装器的情况下从 Java 代码中调用它们。只有在不包含任何实现代码的情况下,Traits 才能完全互操作


非常重要的附录:一个类可以继承多个特征,但只能继承一个抽象类。我认为这应该是开发人员在考虑在几乎所有情况下使用哪个时提出的第一个问题。
lifesaver:“只有在不包含任何实现代码的情况下,Traits 才能完全互操作”
抽象的 - 当集体行为定义或导致一个对象(对象的分支)但仍未组成为(准备好)对象时。特征,当您需要引入能力时,即能力从不源于对象的创建,当对象从孤立中出来并必须进行通信时,它会演变或需要。
想一想,Java8 中不存在第二个区别。
根据 Scala 2.12,特征编译为 Java 8 接口 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces
Y
Yawar

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

每当您实现可重用的行为集合时,您都必须决定是要使用特征还是抽象类。没有固定的规则,但本节包含一些需要考虑的指导方针。如果该行为不会被重用,则将其设为具体类。毕竟这不是可重用的行为。如果它可以在多个不相关的类中重用,请将其设为 trait。只有特征可以混合到类层次结构的不同部分。如果要在 Java 代码中继承它,请使用抽象类。由于带有代码的特征没有与 Java 相似的类似物,因此从 Java 类中的特征继承往往很尴尬。同时,从 Scala 类继承与从 Java 类继承完全一样。作为一个例外,只有抽象成员的 Scala trait 会直接转换为 Java 接口,因此即使您希望 Java 代码继承自它,您也应该可以随意定义此类 trait。有关一起使用 Java 和 Scala 的更多信息,请参阅第 29 章。如果您计划以编译的形式分发它,并且您希望外部组编写继承自它的类,您可能倾向于使用抽象类。问题是当一个 trait 获得或失去一个成员时,任何从它继承的类都必须重新编译,即使它们没有改变。如果外部客户端只会调用行为,而不是从它继承,那么使用特征就可以了。如果效率非常重要,请倾向于使用类。大多数 Java 运行时使类成员的虚拟方法调用比接口方法调用更快。特征被编译为接口,因此可能会产生轻微的性能开销。但是,只有在您知道所讨论的特征构成性能瓶颈并且有证据表明使用类实际上可以解决问题时,您才应该做出此选择。如果您仍然不知道,在考虑了上述内容之后,请先将其作为一个特征。您以后可以随时更改它,并且通常使用特征可以打开更多选项。

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

另一个区别是 super 的处理。

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

有关更多详细信息,请参阅 Chapter 12 的其余部分。

编辑 1 (2013):

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

编辑 2(2018 年):

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

从 Scala 2.12 开始,traits compile to Java interfaces,所以要求放宽了一点。如果 trait 执行以下任何操作,其子类仍需要重新编译:

定义字段(val 或 var,但可以使用常量 – 没有结果类型的最终 val)在主体中调用超级初始化语句 扩展依赖于线性化的类以在正确的超级特征中找到实现

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


If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine - 有人可以解释这里有什么区别吗? extendswith
@0fnt 他的区别不在于扩展与使用。他的意思是,如果你只在同一个编译中混合特征,二进制兼容性问题就不适用。但是,如果您的 API 旨在允许用户自己混合 trait,那么您将不得不担心二进制兼容性。
@0fnt:extendswith 之间绝对没有语义差异。它是纯粹的语法。如果您从多个模板继承,第一个获得 extend,所有其他模板获得 with,仅此而已。将 with 视为逗号:class Foo extends Bar, Baz, Qux
这在 scala 中意味着什么 Trait 可以添加到对象实例中。不能将抽象类添加到对象实例。
D
Daniel C. Sobral

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


C
Community

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

来自 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

p
peter p

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


这是否有任何实际意义,还是只会使代码更容易理解?
M
Marten

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


D
Dario

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


希望你不是暗示特质不能包含行为。两者都可以包含实现代码。
@Mitch Blevins:当然不是。它们可以包含代码,但是当您使用大量辅助函数定义 trait Enumerable 时,我不会称它们为 behaviour,而只是与一个特性相关的功能。
@Dario我将“行为”和“功能”视为同义词,因此我发现您的答案非常令人困惑。
p
pavan.vn101

一个类可以继承多个特征,但只能继承一个抽象类。抽象类可以具有构造函数参数以及类型参数。特征只能有类型参数。例如,你不能说 trait t(i: Int) { }; i 参数是非法的。抽象类可以与 Java 完全互操作。您可以在没有任何包装器的情况下从 Java 代码中调用它们。只有在不包含任何实现代码的情况下,特征才能完全互操作。