1. 简介
Trait 是一种可重用的组件,用于扩展类的行为。它与接口类似,可以包含抽象方法和具体方法,也可以包含属性。
在本教程中,我们将探讨如何创建和扩展 Trait。
2. 示例
我们以建模电影配乐为例,来理解 Trait 的使用方式。
2.1. 创建与扩展 Trait
一首音乐 score(乐谱)需要有 composition(作曲)。我们首先创建一个 Composition Trait:
trait Composition {
var composer: String
def compose(): String
}
接着,我们扩展这个 Trait,创建一个 Score 类:
class Score(var composer: String) extends Composition {
override def compose(): String = s"The score is composed by $composer"
}
✅ 要点:当我们扩展一个 Trait 时,必须实现其中的所有抽象成员(包括方法和属性)。如果我们不想实现,可以将继承类声明为 abstract
。
2.2. 扩展多个 Trait
一首 Score 也需要声音制作。我们创建一个 SoundProduction Trait:
trait SoundProduction {
var engineer: String
def produce(): String
}
然后让 Score 类同时扩展两个 Trait:
class Score(var composer: String, var engineer: String)
extends Composition with SoundProduction {
override def compose(): String = s"The score is composed by $composer"
override def produce(): String = s"The score is produced by $engineer"
}
⚠️ 注意:在继承多个 Trait 时,只能使用 extends
关键字继承第一个 Trait,后续的 Trait 必须使用 with
关键字。
2.3. Trait 继承另一个 Trait
一个 Composition 需要 Orchestration(编曲)和 Mixing(混音)。我们分别创建这两个 Trait:
trait Orchestration {
var orchestra: String
}
trait Mixing {
var mixer: String
}
然后修改 Composition Trait,使其继承这两个 Trait:
trait Composition extends Orchestration with Mixing {
var composer: String
def compose(): String
}
由于 Composition 本身是 Trait,它不需要强制实现父 Trait 的抽象成员。我们可以在 Score 类中实现:
class Score(var composer: String,
var engineer: String,
var orchestra: String,
var mixer: String)
extends Composition with SoundProduction {
override def compose(): String =
s"""The score is composed by $composer,
|Orchestration by $orchestra,
|Mixed by $mixer""".stripMargin
override def produce(): String = s"The score is produced by $engineer"
}
2.4. 覆盖具体成员
每个 Mixing 需要一个质量比和一个混音算法。我们可以在 Mixing Trait 中定义具体成员提供默认实现:
val qualityRatio: Double = 3.14
def algorithm: String = "High instrumental quality"
⚠️ 覆盖具体成员是可选的。我们可以在 Score 类中覆盖这两个成员:
class Score(var composer: String,
var engineer: String,
var orchestra: String,
var mixer: String,
override val qualityRatio: Double)
extends Composition with SoundProduction {
override def algorithm(): String = {
if (qualityRatio < 3) "Low instrumental quality"
else super.algorithm
}
}
2.5. 测试
我们实例化 Score 类并测试各个方法:
class ScoreUnitTest {
@Test
def givenScore_whenComposeCalled_thenCompositionIsReturned() = {
val composer = "Hans Zimmer"
val engineer = "Matt Dunkley"
val orchestra = "Berlin Philharmonic"
val mixer = "Dave Stewart"
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 10, studio)
assertEquals(score.compose(),
s"""The score is composed by $composer,
|Orchestration by $orchestra,
|Mixed by $mixer""".stripMargin)
}
@Test
def givenScore_whenProduceCalled_thenSoundProductionIsReturned() = {
val composer = "Hans Zimmer"
val engineer = "Matt Dunkley"
val orchestra = "Berlin Philharmonic"
val mixer = "Dave Stewart"
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 3, studio)
assertEquals(score.produce(), s"The score is produced by $engineer")
}
@Test
def givenScore_whenLowQualityRatioSet_thenCorrectAlgorithmIsReturned() = {
val composer = "Hans Zimmer"
val engineer = "Matt Dunkley"
val orchestra = "Berlin Philharmonic"
val mixer = "Dave Stewart"
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 1, studio)
assertEquals(score.algorithm(), "Low instrumental quality")
}
@Test
def givenScore_whenHighQualityRatioSet_thenCorrectAlgorithmIsReturned() = {
val composer = "Hans Zimmer"
val engineer = "Matt Dunkley"
val orchestra = "Berlin Philharmonic"
val mixer = "Dave Stewart"
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 10, studio)
assertEquals(score.algorithm(), "High instrumental quality")
}
}
2.6. 动态混入 Trait
有时某个 Score 实例还需要 Vocals(人声),但不是所有实例都需要。我们可以创建一个 Vocals Trait:
trait Vocals {
val sing: String = "Vocals mixin"
}
然后将 Trait 动态混入对象实例:
val score = new Score(composer, engineer, orchestra, mixer, 10) with Vocals
assertEquals(score.sing, "Vocals mixin")
✅ Scala 支持将 Trait 直接附加到对象实例上,实现灵活扩展。
2.7. 限制继承 Trait 的类
假设 SoundProduction 只能被由唱片公司资助的 Score 使用。我们可以创建一个 RecordLabel 类:
class RecordLabel
然后在 SoundProduction Trait 中限制其使用:
trait SoundProduction {
this: RecordLabel =>
// Other methods previously defined
}
这样,只有继承了 RecordLabel 的类才能扩展 SoundProduction。
3. 多继承冲突解决
在 Score 中,Composition 和 SoundProduction 可能在不同的录音室完成。我们为两者添加 getStudio
方法:
var studio: String
def getStudio(): String = s"Composed at studio $studio"
var studio: String
def getStudio(): String = s"Produced at studio $studio"
在 Score 类中覆盖方法:
override def getStudio(): String = super.getStudio()
由于两个 Trait 中有同名方法,Scala 会按照 右优先、深度优先 的顺序解析冲突。
3.1. 默认冲突解析
默认情况下,Scala 会调用最右边的 Trait 中的方法。我们可以验证:
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 10, studio)
assertEquals(score.getStudio(), s"Produced at studio $studio")
3.2. 显式冲突解决
我们可以显式指定调用哪个 Trait 的方法:
override def getStudio(): String =
super[Composition].getStudio() + ", " + super[SoundProduction].getStudio()
测试验证:
assertEquals(
score.getStudio(),
s"Composed at studio $studio, Produced at studio $studio"
3.3. 与 Java 8 接口的对比
✅ Scala Trait 在 2.12 后可以编译为 Java 8 的接口,支持默认方法。但 Java 中没有自动冲突解决机制,需要手动使用 super
明确指定。
4. 密封 Trait(Sealed Trait)
我们可以通过密封 Trait 来表示枚举类型。例如:
sealed trait MixingAlgorithm
case object LowInstrumentalQuality extends MixingAlgorithm {
override def toString(): String = "Low instrumental quality"
}
case object HighInstrumentalQuality extends MixingAlgorithm {
override def toString(): String = "High instrumental quality"
}
修改 Trait 和类:
def algorithm: MixingAlgorithm = HighInstrumentalQuality
override def algorithm(): MixingAlgorithm = {
if (qualityRatio < 3) LowInstrumentalQuality
else super.algorithm
}
测试验证:
assertEquals(score.algorithm().toString, "High instrumental quality")
✅ 密封 Trait 的特点:
- 只能在同一文件中扩展
- 编译器可进行穷举检查,避免遗漏匹配项
5. Trait 与抽象类对比
Trait 和抽象类都支持代码复用,但有关键区别。
5.1. 多继承支持
Trait 支持多继承,而抽象类不支持:
trait Trait1 {
def method1(): String
}
trait Trait2 {
def method2(): String
}
class MultipleInheritance extends Trait1 with Trait2 {
override def method1(): String = "Trait1 method"
override def method2(): String = "Trait2 method"
}
测试验证:
val instance = new MultipleInheritance()
instance.method1() shouldEqual "Trait1 method"
instance.method2() shouldEqual "Trait2 method"
5.2. 构造参数支持
✅ Scala 3 之前,Trait 不支持构造参数,而抽象类支持。Scala 3 开始支持 Trait 参数:
trait Writer(val name: String) {
def introduce = s"Hello, I'm $name"
def write(): String
}
class Author(name: String) extends Writer(name) {
def write(): String = s"$name is writing a book"
}
class Poet(name: String) extends Writer(name) {
def write(): String = s"$name is composing poetry"
}
测试验证:
val author = new Author("Mark Twain")
author.introduce shouldEqual "Hello, I'm Mark Twain"
6. 总结
本文详细介绍了 Scala 中 Trait 的使用方式、与抽象类的异同、冲突处理机制及密封 Trait 的使用。Trait 是 Scala 中实现代码复用和模块化设计的重要工具。
完整代码可在 GitHub 获取。