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
即使 OptionY
是 MultipleChoice
的子类,且定义在另一个文件中。
为了避免这种情况,我们可以使用 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 上找到。