Что такое sealed class/trait и зачем она нужна?
В Scala ключевое слово sealed используется для ограничения иерархии наследования классов и трейтов. Оно указывает компилятору, что все подклассы данного класса или трейта должны быть определены в одном и том же исходном файле, что даёт множество преимуществ как в плане безопасности, так и в плане удобства работы с сопоставлением с образцом (pattern matching).
Синтаксис и пример
sealed trait Expr
case class Num(n: Int) extends Expr
case class Add(a: Expr, b: Expr) extends Expr
В этом примере Expr — это sealed trait, и все его возможные реализации (Num, Add) определены в том же файле.
Зачем нужен sealed
1. Полнота (исчерпываемость) сопоставления с образцом
Компилятор знает, какие классы являются всеми возможными подтипами sealed-типа, и может проверять, что match выражение покрывает все случаи.
def eval(expr: Expr): Int = expr match {
case Num(n) => n
case Add(a, b) => eval(a) + eval(b)
// если бы не было sealed, компилятор не знал бы — все ли варианты учтены
}
Если вы забудете вариант — компилятор предупредит.
2. Безопасность типов
sealed помогает построить замкнутую иерархию, которую нельзя расширить вне данного файла. Это особенно важно при проектировании доменной модели или DSL (domain-specific language), где вы хотите контролировать все допустимые варианты значений.
3. Поддержка в IDE и инструментовке
IDE (например, IntelliJ IDEA) может подсказывать все возможные варианты для sealed-типа, автогенерировать match выражения с exhaustiveness checking.
4. Улучшение читаемости и поддержки кода
Программисту не нужно беспокоиться о том, что в другом месте кода кто-то добавит новый подкласс, который нарушит логику обработки. Всё, что может быть — видно на месте.
Отличия от final и abstract
-
final запрещает наследование: нельзя унаследовать или переопределить final-класс.
-
abstract требует, чтобы класс был расширен: он неполный, требует реализации.
-
sealed разрешает наследование, но только внутри одного файла.
Отличие sealed и sealed abstract
Обычно sealed trait используется, но можно писать sealed abstract class, если нужны общие поля или логика.
sealed abstract class Shape(val name: String)
case class Circle(r: Double) extends Shape("circle")
case class Square(s: Double) extends Shape("square")
Расширение иерархии — только внутри одного файла
// Файл: Expr.scala
sealed trait Expr
case class Num(n: Int) extends Expr
case class Add(a: Expr, b: Expr) extends Expr
// В другом файле:
// case class Sub(a: Expr, b: Expr) extends Expr // Ошибка!
Совместимость с enum (начиная с Scala 3)
В Scala 3 enum автоматически является sealed:
enum Expr:
case Num(n: Int)
case Add(a: Expr, b: Expr)
Здесь Expr — sealed, и расширение возможно только в этом файле.
Использование в ADT (алгебраических типах данных)
sealed trait + case class — это стандартная практика моделирования доменной логики в функциональном стиле.
sealed trait Option\[+A\]
case class Some\[A\](value: A) extends Option\[A\]
case object None extends Option\[Nothing\]
Это позволяет задать чёткую структуру значений с безопасной и предсказуемой обработкой через match.
Возможность комбинации с case class
Часто sealed используется вместе с case class или case object, так как эти классы автоматически получают:
-
методы equals, hashCode, toString
-
unapply (для pattern matching)
-
копирование через copy
Уточнение: sealed в Scala 3
В Scala 3 появились модификаторы sealed и open, а также новое ключевое слово enum, но смысл sealed остался прежним — ограничить наследование одним файлом.
Краткие тезисы (без выделения итогов):
-
sealed ограничивает расширение класса или трейта только текущим исходным файлом.
-
Обеспечивает безопасность и контроль иерархии.
-
Позволяет компилятору проверять полноту pattern matching.
-
Часто используется в моделировании алгебраических типов данных.
-
Не позволяет расширять sealed-типы в других файлах проекта.
-
Хорошо сочетается с case class, case object и паттерн-матчингом.