什么是密封特质(sealed trait)?

374

《Scala编程》中有关于密封类的描述,但并没有提到密封特质。 在哪里可以找到更多关于密封特质的信息?

我想知道,密封特质和密封类是否相同? 如果不同,它们之间有什么区别? 何时使用密封特质是一个好主意(何时不是)?

6个回答

526

sealed特质只能在声明它的同一个文件中被扩展。

它们通常用于提供对enums的替代方案。由于它们只能在单个文件中进行扩展,编译器可以知道所有可能的子类型并进行推理。

例如,使用以下声明:

sealed trait Answer
case object Yes extends Answer
case object No extends Answer
编译器会在匹配不全时发出警告:
scala> val x: Answer = Yes
x: Answer = Yes

scala> x match {
     |   case No => println("No")
     | }
<console>:12: warning: match is not exhaustive!
missing combination            Yes

如果可能的子类型数量是有限且预先已知的,那么应该使用sealed traits(或sealed abstract class)。更多示例请参见列表选项实现。


135
我花了六个月的时间偶然到达这里,并理解如何在Scala中替换Java Enum。 - sscarduzio
1
非常好!不仅有限且预先已知,而且是在受到限制(封闭?)的上下文中,检查所有可能的子类型是否有意义,例如是|否,偶数|奇数等。 - Mário de Sá Vera
3
Scala3终于有了枚举 https://alvinalexander.com/scala/example-enums-in-scala-3-dotty-match-expression/ - Kjetil S.

98
一个被封装的特质(sealed trait)和一个被封装的类(sealed class)是相同的吗?就“sealed”而言,是的。当然,它们之间共享着“trait”和“class”的一般区别。 如果不是这样,它们有什么区别呢?无关紧要。 何时使用封装的特质是个好主意(什么情况下不是)?如果你有一个“sealed class X”,那么你必须检查“X”以及任何子类。但“sealed abstract class X”或“sealed trait X”则并非如此。所以你可以使用“sealed abstract class X”,但那比只用“trait”冗长得多,却没有太多优势。 使用“abstract class”的主要优势是它可以接收参数。这个优点在使用类型类时特别相关。比如说,你想建立一个排序树。你可以这样写:
sealed abstract class Tree[T : Ordering]

但是你不能做到这一点:
sealed trait Tree[T : Ordering]

由于上下文限定(和视图限定)是使用隐式参数实现的。鉴于特质不能接收参数,因此你无法这样做。

个人而言,我更喜欢使用sealed trait,除非某些特殊原因让我使用sealed abstract class。我不是在谈论微妙的原因,而是一些明显的原因,你无法忽略,例如使用类型类。


"由于上下文边界(和视图边界)是通过隐式参数实现的。" - 你能详细解释一下吗? - Irina Rapoport
@Ruby - 回复有点晚,但如果你或其他人感兴趣:上下文边界([A:F])的工作方式与方差约束不同。相反,它是一种语法糖,要求在范围内有一个隐式的F [A]。通常用于以比隐式参数((implicit fa:F [A]))更简洁和易读的方式召唤类型类实例,但在幕后它仍然以完全相同的方式工作,正如Daniel所指出的那样,特质无法做到这一点。 - mirichan
在撰写本文时,这是唯一回答该问题(密封特质 vs 密封类)的答案。其他答案回答的是一个不同的问题,而非所问的(密封 vs 非密封)。 - 5fec

58

来自daily-scala博客:

当一个trait被“sealed”(密封)时,所有子类都必须在同一个文件中声明,这使得子类的集合变为有限,从而允许进行某些编译器检查。


谢谢。"所有的子类"是指类和特质吗? - John Threepwood
@John - 我没有尝试过,但我怀疑是类。密封的重点在于所有内容都在一个源单元中定义。 - Brian Agnew
1
@JohnThreepwood:类、特质和对象。在Scala中,大多数情况下,“类”一词用于指代类、特质和对象。只有在讨论它们之间的具体区别时,才表示指类。SLS使用术语“模板”来指代类和特质,但该术语在SLS之外很少使用,并且没有一个术语包括类、特质和对象三者。 - Jörg W Mittag

31

9

‌‌简而言之:

  • 密封特质只能在同一文件中扩展
  • 使用列表可让编译器轻松了解所有可能的子类型
  • 当可能的子类型数量是有限且预先已知时,使用密封特质
  • 这是创建类似于Java枚举的一种方式
  • 帮助定义代数数据类型(ADTs)

更多详情请参见 Scala中关于密封特质的一切


5
特质(Traits)也可以定义为密封的(sealed),并且只能由一组固定的case classes来扩展。 普通特质和密封特质之间的核心区别可以总结如下:
  • 普通特质是开放的,因此任何数量的类都可以继承该特质,只要它们提供了所有必需的方法,并且这些类的实例可以通过特质所需的方法互换使用。 普通特质层次结构使添加额外的子类变得容易:只需定义您的类并实现必要的方法。但是,添加新方法变得困难:需要将新方法添加到所有现有子类中,而现有子类可能有很多。

  • 密封特质是封闭的:它们仅允许一组固定的类从中继承,并且所有继承类必须与特质本身一起在同一文件或REPL命令中定义。 密封特质层次结构相反:添加新方法很容易,因为新方法只需模式匹配每个子类并决定要为每个子类执行什么操作。但是,添加新子类很困难,因为您需要转到所有现有模式匹配并添加处理新子类的case。

例如:

object SealedTraits extends App{
  sealed trait Point
  case class Point2D(x: Double, y: Double) extends Point
  case class Point3D(x: Double, y: Double, z: Double) extends Point

  def hypotenuse(p: Point) = p match {
    case Point2D(x, y) => math.sqrt(x  x + y  y)
    case Point3D(x, y, z) => math.sqrt(x  x + y  y + z  z)
  }

  val points: Array[Point] = Array(Point2D(1, 2), Point3D(4, 5, 6))

  for (p <- points) println(hypotenuse(p))
  // 2.23606797749979
  // 8.774964387392123

一般来说,sealed traits 适合用于建模层次结构,其中你预计子类的数量很少或根本不变。可以使用sealed trait 来建模的一个很好的例子是JSON
  • JSON只能是JSON null、布尔值、数字、字符串、数组或字典。
  • JSON保持了20年的稳定,因此不太可能有人需要扩展我们的JSON以添加其他子类。
  • 虽然子类集合是固定的,但是我们可能想要对JSON blob执行的操作范围是无限的:解析它、序列化它、漂亮地打印它、缩小它、消毒它等等。 因此,将JSON数据结构建模为封闭的sealed trait层次结构而不是普通的开放的trait层次结构是有意义的。
  sealed trait Json
  case class Null() extends Json
  case class Bool(value: Boolean) extends Json
  case class Str(value: String) extends Json
  case class Num(value: Double) extends Json
  case class Arr(value: Seq[Json]) extends Json
  case class Dict(value: Map[String, Json]) extends Json

这个问题是在问sealed trait和sealed class之间的区别,但是这个回答却回答了另一个问题(sealed vs non-sealed traits的区别)。 - 5fec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接