--- title: Sealed class и interface description: Закрытые иерархии, исчерпывающий when, выбор между sealed class и interface tags: - kotlin - oop --- `sealed` означает: все наследники известны компилятору. Новый наследник может появиться только в том же модуле и пакете. ```kotlin sealed interface Result2 data class Success(val data: String) : Result2 data class Failure(val error: Throwable) : Result2 data object Loading : Result2 fun render(r: Result2): String = when (r) { is Success -> "данные: ${r.data}" is Failure -> "ошибка: ${r.error.message}" Loading -> "загрузка" } fun main() { println(render(Success("ok"))) // данные: ok println(render(Failure(RuntimeException("сбой")))) // ошибка: сбой println(render(Loading)) // загрузка } ``` `else` не нужен: компилятор видит все три варианта. Добавление четвёртого сломает компиляцию во всех `when` - это и есть цель. ## Зачем это нужно `sealed` - способ выразить «одно из» в системе типов. Без него альтернативой был бы класс со всеми полями сразу и флагом: ```kotlin // плохо: половина полей всегда null class BadResult(val data: String?, val error: Throwable?, val loading: Boolean) ``` Такой класс позволяет создать бессмысленные состояния: данные и ошибка одновременно. `sealed` их запрещает на уровне типов. ## sealed class или sealed interface ```kotlin sealed class Shape { abstract val area: Double // общее состояние и поведение } class Circle(val r: Double) : Shape() { override val area get() = Math.PI * r * r } class Square(val side: Double) : Shape() { override val area get() = side * side } fun main() { val shapes: List = listOf(Circle(1.0), Square(2.0)) println(shapes.sumOf { it.area }) // 7.141592653589793 } ``` | | `sealed class` | `sealed interface` | |---|---|---| | состояние (свойства с полем) | можно | нет | | конструктор | есть | нет | | наследник может входить в другую иерархию | нет (один супертип-класс) | да | | `data class` как наследник | да | да | Правило: берите `sealed interface`, если общего состояния нет. Он гибче - один тип может реализовать несколько sealed-интерфейсов: ```kotlin sealed interface Error2 sealed interface Retryable data class Timeout(val seconds: Int) : Error2, Retryable data class NotFound(val id: Int) : Error2 fun describe(e: Error2) = when (e) { is Timeout -> "таймаут, можно повторить: ${e is Retryable}" is NotFound -> "не найдено ${e.id}" } fun main() { println(describe(Timeout(30))) // таймаут, можно повторить: true println(describe(NotFound(1))) // не найдено 1 } ``` ## Где можно объявлять наследников С Kotlin 1.5 наследники должны быть в том же **модуле** и **пакете**, но не обязательно в том же файле: ```kotlin // файл Result.kt sealed interface Result2 // файл Success.kt — тот же пакет, тот же модуль // data class Success(val data: String) : Result2 ``` Раньше требовался один файл. Сейчас обычно всё равно держат в одном - иерархию видно целиком. Вложенное объявление - распространённый стиль: ```kotlin sealed interface NetworkResult { data class Ok(val body: String) : NetworkResult data class HttpError(val code: Int) : NetworkResult data object Offline : NetworkResult } fun handle(r: NetworkResult) = when (r) { is NetworkResult.Ok -> r.body is NetworkResult.HttpError -> "HTTP ${r.code}" NetworkResult.Offline -> "нет сети" } fun main() { println(handle(NetworkResult.HttpError(404))) // HTTP 404 } ``` Плюс: имена не загрязняют пакет и читаются как `NetworkResult.Offline`. ## Исчерпывающий when Проверка работает и для `when`-инструкции (без результата) - с Kotlin 2.0: ```kotlin sealed interface Event data object Start : Event data object Stop : Event fun log(e: Event) { when (e) { // инструкция, не выражение Start -> println("старт") Stop -> println("стоп") } // else не нужен, полнота проверяется } fun main() { log(Start) } // старт ``` Для nullable-типа нужна ветка `null`: ```kotlin sealed interface Event data object Start : Event fun log(e: Event?) = when (e) { Start -> "старт" null -> "ничего" } fun main() { println(log(null)) } // ничего ``` ## Иерархия глубже одного уровня Промежуточный узел тоже может быть `sealed`: ```kotlin sealed interface Expr data class Num(val value: Int) : Expr sealed interface BinOp : Expr { val left: Expr val right: Expr } data class Add(override val left: Expr, override val right: Expr) : BinOp data class Mul(override val left: Expr, override val right: Expr) : BinOp fun eval(e: Expr): Int = when (e) { is Num -> e.value is Add -> eval(e.left) + eval(e.right) is Mul -> eval(e.left) * eval(e.right) } fun main() { println(eval(Add(Num(2), Mul(Num(3), Num(4))))) // 14 } ``` Можно сопоставлять и на промежуточном уровне: ```kotlin sealed interface Expr data class Num(val value: Int) : Expr sealed interface BinOp : Expr data class Add(val l: Expr, val r: Expr) : BinOp data class Mul(val l: Expr, val r: Expr) : BinOp fun kind(e: Expr) = when (e) { is Num -> "литерал" is BinOp -> "операция" // покрывает Add и Mul } fun main() { println(kind(Add(Num(1), Num(2)))) } // операция ``` ## Перебор наследников ```kotlin sealed interface Status { data object Active : Status data object Blocked : Status } fun main() { println(Status::class.sealedSubclasses.map { it.simpleName }) // [Active, Blocked] } ``` Работает через рефлексию и требует `kotlin-reflect`. Для фиксированных объектов проще держать список руками. Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md) ## Типичные применения **Результат операции** вместо исключений: ```kotlin sealed interface Parsed { data class Ok(val value: T) : Parsed data class Err(val message: String) : Parsed } fun parse(s: String): Parsed = s.toIntOrNull()?.let { Parsed.Ok(it) } ?: Parsed.Err("не число: $s") fun main() { println(when (val r = parse("abc")) { is Parsed.Ok -> "значение ${r.value}" is Parsed.Err -> r.message }) // не число: abc } ``` Обратите внимание на `Parsed` для варианта без данных - это работает благодаря `out T` и тому, что `Nothing` подтип всего. Подробнее: [Any, Unit, Nothing](Any,%20Unit,%20Nothing.md), [Вариантность](../../Дженерики/Вариантность.md) **Состояние UI**, **события**, **команды**, **узлы AST** - везде, где набор вариантов фиксирован и у каждого свои данные. > [!info] Отличия от Java > В Java аналог появился в 17 - `sealed` с явным `permits`: > ```java > sealed interface Result permits Success, Failure {} > record Success(String data) implements Result {} > record Failure(Throwable error) implements Result {} > ``` > > | Java 17+ | Kotlin | > |---|---| > | `sealed interface X permits A, B` | `sealed interface X` (список выводится) | > | наследники в том же пакете/модуле | то же | > | `switch` с pattern matching (Java 21) | `when` с `is` | > | проверка полноты в `switch` (Java 21) | есть всегда | > | `non-sealed` для открытия ветки | нет аналога | > > Kotlin не требует перечислять наследников: компилятор находит их сам в пределах модуля. Это удобнее при добавлении варианта - править объявление не нужно. > > До Java 17 то же выражали абстрактным классом с приватным конструктором или паттерном Visitor. Visitor даёт проверку полноты (интерфейс с методом на каждый тип), но требует много кода и меняет все реализации при добавлении варианта. > > Сравнить: [Абстрактные классы в Java](../../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md), [Полиморфизм подтипов](../../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md) > [!warning] Подводные камни > **`else` в `when` убивает проверку полноты.** Главная ошибка: > ```kotlin > sealed interface Event > data object Start : Event > data object Stop : Event > > fun log(e: Event) = when (e) { > Start -> "старт" > else -> "прочее" // добавление варианта пройдёт незамеченным > } > > fun main() { println(log(Stop)) } // прочее > ``` > Если ветвей много и большинство обрабатывается одинаково, лучше перечислить их через запятую, чем писать `else`. > > **Проверка полноты не работает из Java.** Java-код, читающий sealed-иерархию Kotlin, обязан иметь `default` в `switch`. > > **`when` по `is` без исчерпывающего контекста не проверяется.** Полнота требуется только там, где `when` - выражение или где компилятор знает, что иерархия закрыта. Для не-sealed типа проверки нет вообще. > > **`sealed class` требует вызова конструктора в наследнике:** > ```kotlin > sealed class Base(val id: Int) > class Child(id: Int) : Base(id) // скобки обязательны > > fun main() { println(Child(1).id) } // 1 > ``` > У `sealed interface` конструктора нет - на одну причину для ошибки меньше. > > **Наследник в другом модуле не скомпилируется**, даже если пакет совпадает. Это ограничивает расширяемость библиотечного API: если вариантов должно быть больше, `sealed` не подходит. > > **`sealed` класс сам абстрактен** - создать его нельзя: > ```kotlin > sealed class Shape > // val s = Shape() // ошибка: sealed-класс абстрактный > class Circle : Shape() > > fun main() { println(Circle()::class.simpleName) } // Circle > ``` > > **Для варианта без данных нужен `data object`, а не `class`:** > ```kotlin > sealed interface State > class Loading1 : State // каждый экземпляр разный > data object Loading2 : State // единственный экземпляр > > fun main() { > println(Loading1() == Loading1()) // false — ловушка > println(Loading2 == Loading2) // true > } > ``` > > **Дженерик-наследник sealed-типа требует внимания к вариантности.** Без `out` вариант без данных нельзя объявить как `Err : Parsed`. Дополнительно: [Enum class](Enum%20class.md) Далее: [Value class](Value%20class.md)