1. 概述

在 Scala 中,使用 final 修饰符可以禁止类或特质(trait)被继承,这种方式非常严格。而如果将类设为 public,则任何其他类都可以继承它。

如果我们想要一个介于两者之间的方案呢?这时候,Scala 的 sealed 关键字就派上用场了!

在本教程中,我们将学习 Scala 中的 sealed 关键字是什么、它的用途以及如何正确使用它。

2. 什么是 sealed 关键字?

sealed 关键字用于控制类和特质的继承。将类或特质声明为 sealed 后,其子类必须定义在同一个源文件中

在下一节中,我们会看到 sealed 修饰符的一些特性与行为,比如在不同场景下使用时编译器会抛出的警告或异常。

3. sealed 的特性

让我们通过一个示例,在文件 SealedClassExample.scala 中使用 sealed 实现一个多选题选项类:

sealed abstract class MultipleChoice

case class OptionA() extends MultipleChoice
case class OptionB() extends MultipleChoice
case class OptionC() extends MultipleChoice

如果尝试在父类文件之外扩展一个 sealed 特质,编译器会报错:

illegal inheritance from sealed class.

我们新建一个源文件 SealedExtendedDifferentFile.scala 并做如下尝试:

case class OptionD() extends MultipleChoice

此时编译器会报错:

Error:(5, 32) illegal inheritance from sealed class MultipleChoice
case class OptionD() extends MultipleChoice

不过,sealed 类的子类再继承是允许的

case class OptionY() extends OptionX  // ✅ 合法

这种二级子类也可以用于模式匹配。如果我们不把它加入 match 分支,编译器也不会发出警告:

case class OptionY() extends OptionX

def selectOption(option: MultipleChoice): String = option match {
  case optionY: OptionY => "Option-Y Selected"
  case optionX: OptionX => "Option-X Selected"
}

println(selectOption(OptionY()))

这段代码会正常执行,输出如下:

Option-Y Selected

即使 OptionYMultipleChoice 的子类,且定义在另一个文件中。

为了避免这种情况,我们可以使用 case class,因为 case class 默认是 final 的,不能被继承。

Scala 允许对特质、类和抽象类使用 sealed 修饰符。在 sealed 上下文中,这三者的功能基本一致,区别仅限于普通类与特质的区别,例如抽象类可以接收构造参数,而特质不行。

4. 使用 sealed 的优势

使用 sealed 类时,我们可以确保只有当前文件中定义的子类存在。这有助于 编译器知道所有可能的子类,在模式匹配等场景中非常有用。

match 表达式没有覆盖所有情况时,编译器可以发出警告以避免运行时抛出 MatchError 异常。我们试着从模式匹配中省略 OptionC,观察编译器的行为:

def selectOption(option: MultipleChoice): String = option match {
  case optionA: OptionA => "Option-A Selected"
  case optionB: OptionB => "Option-B Selected"
}

此时编译器会给出如下警告:

Warning:(11, 54) match may not be exhaustive.
It would fail on the following input: OptionC()
  def selectOption(option: MultipleChoice): String = option match {

✅ 编译器的这个提示非常实用,能有效防止遗漏匹配项。

5. 使用 sealed 替代枚举(Enum)

Scala 中的 枚举(Enumeration) 有一些缺点,比如无法扩展枚举行为、类型擦除后所有枚举值类型相同、编译时无法进行穷举检查等。

使用一组 sealed 的 case object 可以弥补这些问题。sealed 提供了灵活性,既可保持简洁,也可按需添加更多功能。

我们来看如何使用 sealed class 实现一周七天的枚举:

sealed abstract class DayOfTheWeek(val name: String, val isWeekEnd: Boolean)

case object Monday extends DayOfTheWeek("Monday", false)
case object Tuesday extends DayOfTheWeek("Tuesday",  false)
case object Wednesday extends DayOfTheWeek("Wednesday", false)
case object Thursday extends DayOfTheWeek("Thursday", false)
case object Friday extends DayOfTheWeek("Friday", false)
case object Saturday extends DayOfTheWeek("Saturday", true)
case object Sunday extends DayOfTheWeek("Sunday", true)

⚠️ 但需要注意的是,这种方式相比 Scala 的 Enumeration 类也有一些不足,比如缺少默认的序列化/反序列化方法、值的排序、获取所有值列表等功能。

因此,在选择实现方式时,需要根据具体需求进行权衡。

6. sealed 与代数数据类型(ADT)

代数数据类型(Algebraic Data Type,ADT) 是一种复合类型,由一组实现标准接口的固定值组成。ADT 的核心特性是它能确保非法状态无法存在。

ADT 主要分为两种类型:积类型(product type)和类型(sum type)。和类型将一个类型的所有可能值放在一个容器中。

我们来看一个咖啡店的例子,使用 sealed trait 实现一个和类型的 ADT 来表示咖啡种类:

sealed trait Coffee

case object Cappuccino extends Coffee
case object Americano extends Coffee
case object Espresso extends Coffee

✅ 这种方式非常适合表达有限集合的状态,且编译器能帮助我们检查模式匹配的完整性。

7. 总结

在本教程中,我们学习了 Scala 中的 sealed 关键字,了解了它的特性、在模式匹配中的优势,以及它在实现枚举和代数数据类型中的应用。

同时我们也学会了如何在项目中合理使用 sealed,以便写出更安全、更易维护的代码。

一如既往,本文中的所有示例代码都可以在 GitHub 上找到。


原始标题:Sealed Keyword in Scala