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 中,CompositionSoundProduction 可能在不同的录音室完成。我们为两者添加 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 获取。


原始标题:Introduction to Traits in Scala