Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,294 @@
---
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<Shape> = 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<out T> {
data class Ok<T>(val value: T) : Parsed<T>
data class Err(val message: String) : Parsed<Nothing>
}
fun parse(s: String): Parsed<Int> =
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<Nothing>` для варианта без данных - это работает благодаря `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<Nothing>`.
Дополнительно: [Enum class](Enum%20class.md)
Далее: [Value class](Value%20class.md)