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,336 @@
---
title: Result и runCatching
description: Ошибка как значение: Result, runCatching, свои sealed-типы и когда что выбирать
tags:
- kotlin
- exceptions
---
Исключение - не единственный способ сообщить об ошибке. Ошибку можно вернуть как значение, и тогда компилятор заставит её обработать.
Три уровня, от простого к гибкому:
| Способ | Что несёт ошибка | Когда |
|---|---|---|
| `T?` | ничего, только «не получилось» | причина не важна |
| `Result<T>` | `Throwable` | причина нужна, но типизировать её незачем |
| свой `sealed`-тип | что угодно | причин несколько и они разные |
## Nullable как простейший Result
Для одной очевидной причины сбоя `null` достаточно:
```kotlin
fun main() {
println("42".toIntOrNull()) // 42
println("abc".toIntOrNull()) // null
println("abc".toIntOrNull() ?: 0) // 0
}
```
Стандартная библиотека придерживается соглашения: `toInt()` бросает, `toIntOrNull()` возвращает `null`; `first()` бросает, `firstOrNull()` - нет.
```kotlin
fun main() {
val items = listOf(1, 2, 3)
println(items.getOrNull(10)) // null
println(items.firstOrNull { it > 5 }) // null
println(mapOf("a" to 1)["b"]) // null
}
```
Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
## Result
`Result<T>` хранит либо значение, либо `Throwable`.
```kotlin
fun main() {
val ok: Result<Int> = Result.success(42)
val fail: Result<Int> = Result.failure(IllegalStateException("сбой"))
println(ok.isSuccess) // true
println(fail.isFailure) // true
println(ok.getOrNull()) // 42
println(fail.getOrNull()) // null
println(fail.exceptionOrNull()?.message) // сбой
}
```
### runCatching
Оборачивает блок: результат - `Result`, исключение - `failure`.
```kotlin
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
fun main() {
println(parse("42")) // Success(42)
println(parse("abc").exceptionOrNull()?.javaClass?.simpleName) // NumberFormatException
}
```
Есть вариант-расширение с получателем:
```kotlin
fun main() {
val length = "привет".runCatching { length }
println(length.getOrNull()) // 6
}
```
### Извлечение значения
```kotlin
fun main() {
val ok = runCatching { 42 }
val fail = runCatching<Int> { error("сбой") }
println(ok.getOrNull()) // 42
println(fail.getOrDefault(0)) // 0
println(fail.getOrElse { e -> e.message!!.length }) // 4
println(ok.getOrThrow()) // 42
try {
fail.getOrThrow()
} catch (e: IllegalStateException) {
println("пробросилось: ${e.message}") // пробросилось: сбой
}
}
```
### Преобразования
```kotlin
fun main() {
val r = runCatching { "42" }
.map { it.toInt() } // только при успехе
.map { it * 2 }
println(r.getOrNull()) // 84
// mapCatching перехватывает исключение внутри преобразования
val bad = runCatching { "abc" }.mapCatching { it.toInt() }
println(bad.isFailure) // true
// map не перехватывает — исключение улетит наружу
val thrown = runCatching { runCatching { "abc" }.map { it.toInt() } }
println(thrown.isFailure) // true
}
```
Разница существенная: `map` предполагает, что преобразование не падает, `mapCatching` - что может.
```kotlin
fun main() {
val r = runCatching<Int> { error("сбой") }
.recover { 0 } // заменяем ошибку значением
println(r.getOrNull()) // 0
val folded = runCatching { "42".toInt() }
.fold(
onSuccess = { "значение $it" },
onFailure = { "ошибка ${it.message}" },
)
println(folded) // значение 42
}
```
### onSuccess и onFailure
Побочные эффекты без изменения значения - они возвращают сам `Result`:
```kotlin
fun main() {
runCatching { "abc".toInt() }
.onSuccess { println("получили $it") }
.onFailure { println("не вышло: ${it.message}") }
.getOrDefault(0)
.let { println("итог: $it") }
// не вышло: For input string: "abc"
// итог: 0
}
```
## Свой sealed-тип
Когда причин несколько и клиент должен их различать, `Result` слишком беден - в нём любая ошибка это `Throwable`.
```kotlin
sealed interface LoadResult {
data class Success(val user: String) : LoadResult
data object NotFound : LoadResult
data class Forbidden(val reason: String) : LoadResult
data class NetworkError(val retryAfter: Int) : LoadResult
}
fun load(id: Int): LoadResult = when {
id == 1 -> LoadResult.Success("Иван")
id == 2 -> LoadResult.Forbidden("нет прав")
id < 0 -> LoadResult.NetworkError(retryAfter = 5)
else -> LoadResult.NotFound
}
fun describe(r: LoadResult) = when (r) { // else не нужен
is LoadResult.Success -> "загружен ${r.user}"
LoadResult.NotFound -> "не найден"
is LoadResult.Forbidden -> "запрещено: ${r.reason}"
is LoadResult.NetworkError -> "повтор через ${r.retryAfter} с"
}
fun main() {
listOf(1, 2, 3, -1).forEach { println(describe(load(it))) }
// загружен Иван
// запрещено: нет прав
// не найден
// повтор через 5 с
}
```
Главное преимущество - компилятор проверяет, что обработаны все варианты. Добавили новый - все `when` перестанут компилироваться, пока его не учтут. С `Result` такой гарантии нет.
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
### Свой Result с типизированной ошибкой
```kotlin
sealed interface Outcome<out T, out E> {
data class Ok<T>(val value: T) : Outcome<T, Nothing>
data class Err<E>(val error: E) : Outcome<Nothing, E>
}
enum class ParseError { EMPTY, NOT_A_NUMBER, NEGATIVE }
fun parsePositive(s: String): Outcome<Int, ParseError> = when {
s.isBlank() -> Outcome.Err(ParseError.EMPTY)
s.toIntOrNull() == null -> Outcome.Err(ParseError.NOT_A_NUMBER)
s.toInt() < 0 -> Outcome.Err(ParseError.NEGATIVE)
else -> Outcome.Ok(s.toInt())
}
fun main() {
listOf("42", "", "abc", "-1").forEach { input ->
when (val r = parsePositive(input)) {
is Outcome.Ok -> println("'$input' -> ${r.value}")
is Outcome.Err -> println("'$input' -> ошибка ${r.error}")
}
}
// '42' -> 42
// '' -> ошибка EMPTY
// 'abc' -> ошибка NOT_A_NUMBER
// '-1' -> ошибка NEGATIVE
}
```
`Nothing` в позиции второго параметра позволяет `Ok` подходить под любой тип ошибки. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), [Вариантность](../Дженерики/Вариантность.md)
## Что выбрать
```mermaid
graph TD
A["Ошибка возможна"] --> B{"Вызывающий может<br/>что-то сделать?"}
B -->|"нет, это баг"| C["исключение:<br/>require, check, error"]
B -->|"да"| D{"Причину нужно<br/>различать?"}
D -->|"нет"| E["T?"]
D -->|"да, но обработка общая"| F["Result&lt;T&gt;"]
D -->|"да, по-разному"| G["свой sealed-тип"]
```
Практическое разделение:
- **исключение** - для нарушенных предпосылок: неверный аргумент, недопустимое состояние. Это ошибка программиста, её исправляют в коде, а не обрабатывают. Подробнее: [Проверки и контракты](Проверки%20и%20контракты.md)
- **`T?`, `Result`, sealed** - для ожидаемых ситуаций: нет записи в БД, сеть недоступна, ввод пользователя некорректен
## Граница с исключениями
`runCatching` удобен на границе: внешний код бросает, ваш возвращает значение.
```kotlin
class User7(val id: Int, val name: String)
fun fetchFromDb(id: Int): User7 =
if (id == 1) User7(1, "Иван") else throw NoSuchElementException("нет пользователя $id")
fun findUser(id: Int): User7? = runCatching { fetchFromDb(id) }.getOrNull()
fun main() {
println(findUser(1)?.name) // Иван
println(findUser(99)) // null
}
```
Обратное направление - когда API требует исключений, а внутри `Result`:
```kotlin
fun loadOrThrow(id: Int): String = runCatching {
require(id > 0) { "id должен быть положительным" }
"данные $id"
}.getOrThrow()
fun main() {
println(loadOrThrow(1)) // данные 1
try {
loadOrThrow(-1)
} catch (e: IllegalArgumentException) {
println(e.message) // id должен быть положительным
}
}
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | checked exceptions в сигнатуре | `Result<T>` или sealed-тип |
> | `Optional<T>` | `T?` - часть системы типов |
> | `Optional.map`/`orElse` | `?.let`/`?:` |
> | нет аналога `Result` в JDK | `Result<T>` в stdlib |
> | `try`/`catch` для управления | `runCatching` как выражение |
>
> **`Result` частично возвращает то, что отняло отсутствие checked exceptions.** Сигнатура `fun load(): Result<User>` сообщает, что функция может не справиться, и компилятор не даст использовать значение без разбора. Разница с checked exceptions в том, что это выбор автора функции, а не навязанное правило, и такие значения свободно проходят через `map`, `filter` и другие обобщённые функции.
>
> **`T?` вместо `Optional<T>`.** `Optional` - обёртка-объект: аллокация, два уровня значений (`null` и пустой `Optional`), неудобство в полях. Nullable-тип в Kotlin существует только при компиляции, а в байт-коде это обычная ссылка - накладных расходов нет:
> ```kotlin
> fun main() {
> val name: String? = null
> println(name?.uppercase() ?: "<нет>") // <нет>
> }
> ```
> ```java
> Optional<String> name = Optional.empty();
> System.out.println(name.map(String::toUpperCase).orElse("<нет>"));
> ```
>
> **Sealed-типы вместо иерархии исключений.** В Java разные причины сбоя обычно выражают классами исключений и ловят их по типу. Sealed-тип даёт то же различение, но с проверкой полноты при компиляции и без стоимости стектрейса.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **`runCatching` перехватывает всё, включая то, что перехватывать нельзя.** Он ловит `Throwable`, то есть `OutOfMemoryError`, `StackOverflowError` и - самое опасное - `CancellationException` в корутинах:
> ```kotlin
> // Требует kotlinx-coroutines-core.
> // suspend fun broken() {
> // runCatching { delay(1000) } // проглотит отмену корутины
> // }
> //
> // suspend fun fixed() = runCatching { delay(1000) }
> // .onFailure { if (it is CancellationException) throw it }
> ```
> В корутинах либо проверяйте тип вручную, либо используйте обычный `try`/`catch` с перехватом конкретных типов. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
>
> **`Result` нельзя вернуть из функции в публичном API без оговорок.** До Kotlin 1.5 это было запрещено совсем; сейчас разрешено, но тип по-прежнему `value class`, поэтому из Java он виден странно и его нельзя использовать как тип свойства:
> ```kotlin
> class Holder {
> // val r: Result<Int> = Result.success(1) // компилируется, но Result как поле боксируется
> private val value: Int = 1
> fun get(): Result<Int> = Result.success(value)
> }
>
> fun main() { println(Holder().get().getOrNull()) } // 1
> ```
>
> **`Result` стирает тип ошибки.** Все причины сбоя - `Throwable`, поэтому различать их приходится через `is`, и компилятор полноту не проверяет:
> ```kotlin
> fun main() {
> val r = runCatching<Int> { error("сбой") }
> val msg = when (val e = r.exceptionOrNull()) {
> null -> "успех"
> is IllegalStateException -> "состояние: ${e.message}"
> else -> "прочее" // else обязателен
> }
> println(msg) // состояние: сбой
> }
> ```
>
> **Создание `failure` всё равно создаёт исключение со стектрейсом** - то есть `Result` не даёт экономии на производительности, если ошибку формирует `runCatching`. Дешёвый вариант - sealed-тип без `Throwable` внутри.
>
> **Проигнорированный `Result` не вызывает предупреждения:**
> ```kotlin
> fun save(): Result<Unit> = runCatching { error("не записалось") }
>
> fun main() {
> save() // ошибка потеряна молча
> println(save().isFailure) // true — так правильно
> }
> ```
> В отличие от исключения, которое всплывёт само, забытый `Result` ничего не делает. Это основной риск подхода.
>
> **Цепочка `map` на `Result` может бросить.** `map` не перехватывает исключения из своей лямбды - для этого есть `mapCatching`.
>
> **`getOrThrow` на чужом `Result` бросает исходное исключение,** а не обёртку, поэтому стектрейс указывает на место первоначального сбоя, а не на `getOrThrow`. Для диагностики это хорошо, для чтения логов - неожиданно.
>
> **Смешение подходов в одном слое запутывает.** Если часть функций репозитория возвращает `null`, часть `Result`, а часть бросает, клиентский код обрастает тремя видами обработки. Выберите одно соглашение на слой.
Далее: [Проверки и контракты](Проверки%20и%20контракты.md)
@@ -0,0 +1,403 @@
---
title: Проверки и контракты
description: require, check, error, assert и контракты компилятора для умного приведения
tags:
- kotlin
- exceptions
---
Стандартная библиотека даёт четыре функции для проверки предпосылок. Выбор между ними определяется тем, **кто виноват** в нарушении.
| Функция | Бросает | Виноват | Что проверяет |
|---|---|---|---|
| `require` | `IllegalArgumentException` | вызывающий | аргументы |
| `check` | `IllegalStateException` | вызывающий или сам объект | состояние |
| `error` | `IllegalStateException` | - | недостижимая ветка |
| `assert` | `AssertionError` | - | инвариант при отладке |
## require - проверка аргументов
```kotlin
fun withdraw(balance: Long, amount: Long): Long {
require(amount > 0) { "сумма должна быть положительной, получено $amount" }
require(amount <= balance) { "недостаточно средств: $amount > $balance" }
return balance - amount
}
fun main() {
println(withdraw(100, 30)) // 70
try {
withdraw(100, -5)
} catch (e: IllegalArgumentException) {
println(e.message) // сумма должна быть положительной, получено -5
}
}
```
Сообщение передаётся лямбдой, поэтому строка собирается только при нарушении - в успешном случае интерполяция не выполняется.
```kotlin
fun main() {
val n = 5
require(n > 0) // без сообщения: "Failed requirement."
require(n > 0) { "дорогое сообщение: ${(1..1000).sum()}" } // лямбда не вызовется
println("проверки пройдены")
}
```
### requireNotNull
Проверяет и **возвращает** non-null значение:
```kotlin
fun greet(name: String?): String {
val safe = requireNotNull(name) { "имя обязательно" }
return "Привет, ${safe.uppercase()}" // safe имеет тип String
}
fun main() {
println(greet("иван")) // Привет, ИВАН
try {
greet(null)
} catch (e: IllegalArgumentException) {
println(e.message) // имя обязательно
}
}
```
Работает и как инструкция - после вызова переменная умно приводится:
```kotlin
fun process(input: String?): Int {
requireNotNull(input)
return input.length // smart cast: String? -> String
}
fun main() { println(process("abc")) } // 3
```
Это работает благодаря контракту, см. ниже.
## check - проверка состояния
```kotlin
class Connection2 {
private var open = false
fun connect() { open = true }
fun send(data: String): String {
check(open) { "соединение не открыто" }
return "отправлено: $data"
}
}
fun main() {
val c = Connection2()
try {
c.send("данные")
} catch (e: IllegalStateException) {
println(e.message) // соединение не открыто
}
c.connect()
println(c.send("данные")) // отправлено: данные
}
```
Разница с `require` только в типе исключения, но она информативна: `IllegalArgumentException` говорит «ты передал не то», `IllegalStateException` - «ты вызвал не вовремя».
`checkNotNull` - аналог `requireNotNull` для состояния:
```kotlin
class Service18 {
private var config: String? = null
fun init2(value: String) { config = value }
fun run2(): String {
val cfg = checkNotNull(config) { "сначала вызовите init2()" }
return "работаю с $cfg"
}
}
fun main() {
val s = Service18()
try {
s.run2()
} catch (e: IllegalStateException) {
println(e.message) // сначала вызовите init2()
}
s.init2("настройки")
println(s.run2()) // работаю с настройки
}
```
## error - недостижимая ветка
`error(msg)` бросает `IllegalStateException`. Её тип - `Nothing`, поэтому она подходит в любое выражение:
```kotlin
fun describe(code: Int): String = when (code) {
200 -> "ок"
404 -> "не найдено"
500 -> "ошибка сервера"
else -> error("неизвестный код: $code")
}
fun main() {
println(describe(200)) // ок
try {
describe(999)
} catch (e: IllegalStateException) {
println(e.message) // неизвестный код: 999
}
}
```
Благодаря типу `Nothing` компилятор понимает, что ветка не возвращает значение, и `when` остаётся выражением. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
`TODO()` - то же для незаписанного кода:
```kotlin
fun futureFeature(): String = TODO("реализовать после релиза")
fun main() {
try {
futureFeature()
} catch (e: NotImplementedError) {
println(e.message) // An operation is not implemented: реализовать после релиза
}
}
```
`TODO()` возвращает `Nothing`, поэтому подходит на место любого типа - код компилируется, пока функция не написана.
## assert - только для отладки
```kotlin
fun average(values: List<Int>): Double {
val result = values.sum().toDouble() / values.size
assert(result >= values.min()) { "среднее меньше минимума — ошибка в вычислении" }
return result
}
fun main() {
println(average(listOf(1, 2, 3))) // 2.0
}
```
`assert` проверяется **только** при запуске JVM с флагом `-ea` (`-enableassertions`). Без него вызов ничего не делает, но лямбда-сообщение всё равно остаётся объектом в байт-коде.
Практический вывод: `assert` в Kotlin почти не используют. Для проверок, которые должны работать в продакшене, берут `require` и `check`.
## Контракты
Контракт - способ сообщить компилятору то, что он не может вывести сам. Именно поэтому `requireNotNull` даёт умное приведение.
```kotlin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun String?.isNotNullOrBlank(): Boolean {
contract { returns(true) implies (this@isNotNullOrBlank != null) }
return !this.isNullOrBlank()
}
fun main() {
val input: String? = "текст"
if (input.isNotNullOrBlank()) {
println(input.length) // smart cast: String? -> String
}
}
```
Без контракта `input.length` потребовал бы `!!` или `?.` - компилятор не знает, что `true` из функции означает non-null.
Стандартная библиотека использует контракты в `isNullOrEmpty`, `isNullOrBlank`, `require`, `check`, `requireNotNull`:
```kotlin
fun main() {
val s: String? = "abc"
if (!s.isNullOrEmpty()) {
println(s.length) // smart cast работает благодаря контракту
}
val n: Int? = 5
check(n != null)
println(n + 1) // 6, smart cast после check
}
```
### Виды контрактов
```kotlin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun requirePositive(value: Int?) {
contract { returns() implies (value != null) } // вернулась -> value не null
require(value != null && value > 0) { "нужно положительное число" }
}
@OptIn(ExperimentalContracts::class)
inline fun <T> runOnce(block: () -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return block()
}
fun main() {
val x: Int? = 5
requirePositive(x)
println(x + 1) // 6 — smart cast
val value: String // можно не инициализировать...
runOnce { value = "присвоено" } // ...потому что контракт гарантирует один вызов
println(value) // присвоено
}
```
`callsInPlace` с `EXACTLY_ONCE` позволяет присваивать `val` внутри лямбды - компилятор знает, что присваивание произойдёт ровно один раз. На этом работают `run`, `let`, `apply` и `with`.
| Контракт | Что обещает |
|---|---|
| `returns() implies (x)` | если функция вернулась нормально, условие `x` истинно |
| `returns(true) implies (x)` | если вернула `true`, условие `x` истинно |
| `returns(null) implies (x)` | если вернула `null`, условие `x` истинно |
| `callsInPlace(f, AT_MOST_ONCE)` | лямбда вызовется не больше одного раза |
| `callsInPlace(f, EXACTLY_ONCE)` | ровно один раз |
| `callsInPlace(f, AT_LEAST_ONCE)` | хотя бы один раз |
Требования к контракту:
- только для функций верхнего уровня (не для методов класса)
- блок `contract` - **первая** инструкция в теле
- API экспериментальный: нужен `@OptIn(ExperimentalContracts::class)`
- компилятор **не проверяет**, что контракт соответствует коду
Последний пункт - главный риск: контракт это обещание на доверии.
## Валидация данных: исключение или значение
`require` подходит для нарушений, которые означают баг. Для пользовательского ввода лучше вернуть ошибку как значение:
```kotlin
data class Form4(val email: String, val age: Int)
fun validate(email: String, age: Int): List<String> = buildList {
if ("@" !in email) add("email без @")
if (age !in 0..150) add("недопустимый возраст: $age")
}
fun create(email: String, age: Int): Result<Form4> {
val errors = validate(email, age)
return if (errors.isEmpty()) Result.success(Form4(email, age))
else Result.failure(IllegalArgumentException(errors.joinToString("; ")))
}
fun main() {
println(create("a@b.ru", 20).getOrNull()) // Form4(email=a@b.ru, age=20)
println(create("нет", 200).exceptionOrNull()?.message) // email без @; недопустимый возраст: 200
}
```
Разница: `require` падает на первой ошибке, а пользователю обычно нужен список всех. Подробнее: [Result и runCatching](Result%20и%20runCatching.md)
## Инварианты в конструкторе
```kotlin
class Range4(val from: Int, val to: Int) {
init {
require(from <= to) { "неверный диапазон: $from..$to" }
}
val size get() = to - from + 1
}
@JvmInline
value class Email(val value: String) {
init {
require("@" in value) { "не похоже на email: $value" }
}
}
fun main() {
println(Range4(1, 5).size) // 5
println(Email("a@b.ru").value) // a@b.ru
try {
Range4(5, 1)
} catch (e: IllegalArgumentException) {
println(e.message) // неверный диапазон: 5..1
}
}
```
Проверка в конструкторе гарантирует, что невалидного объекта не существует, - дальше по коду проверять не нужно. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md), [Инициализация](../ООП/Инициализация.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `if (x < 0) throw new IllegalArgumentException(...)` | `require(x >= 0) { ... }` |
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` + smart cast |
> | `assert x > 0 : "msg"` | `assert(x > 0) { "msg" }` |
> | Guava `Preconditions.checkArgument` | `require` в stdlib |
> | `throw new UnsupportedOperationException()` | `TODO()` |
> | нет аналога | контракты для smart cast |
>
> **`require` и `check` - это то, за чем в Java идут в Guava.** `Preconditions.checkArgument` и `checkState` из Guava выполняют ту же роль, но требуют внешней зависимости. В Kotlin они в stdlib и вдобавок `inline`, то есть вызова функции в байт-коде не остаётся.
>
> **Сообщение в лямбде экономит работу.** В Java `checkArgument("плохое значение: " + value)` собирает строку всегда, даже когда проверка пройдена. Передача лямбды плюс `inline` означает, что конкатенация происходит только при нарушении:
> ```kotlin
> fun main() {
> val value = 5
> require(value > 0) { "дорогая строка: ${value.toString().repeat(1000).length}" }
> println("лямбда не вызывалась")
> }
> ```
>
> **`requireNotNull` даёт smart cast, `Objects.requireNonNull` - нет.** В Java результат нужно присвоить: `this.name = Objects.requireNonNull(name)`. В Kotlin после проверки компилятор сам считает переменную non-null.
>
> **Контрактов в Java нет совсем.** Ближайший аналог - аннотации `@Nullable`/`@NotNull` и внешние анализаторы, которые работают вне компилятора и не влияют на типизацию.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
> [!warning] Подводные камни
> **`assert` по умолчанию выключен.** Без `-ea` при запуске JVM проверка не выполняется совсем:
> ```kotlin
> fun main() {
> assert(1 == 2) { "это сообщение вы не увидите без -ea" }
> println("программа продолжила работу")
> }
> ```
> Код, который полагается на `assert` для защиты инвариантов в продакшене, не защищён ничем. Используйте `check`.
>
> **Путаница `require` и `check`.** Тип исключения - часть контракта: вызывающий может ловить `IllegalArgumentException`, чтобы отличить «я передал мусор» от «объект не готов». Перепутанные функции делают эту информацию ложной.
>
> **Сообщение без контекста бесполезно:**
> ```kotlin
> fun main() {
> val size = -1
> try {
> require(size >= 0) { "неверный размер" } // какой именно?
> } catch (e: IllegalArgumentException) {
> println(e.message)
> }
> try {
> require(size >= 0) { "размер должен быть >= 0, получено $size" }
> } catch (e: IllegalArgumentException) {
> println(e.message) // размер должен быть >= 0, получено -1
> }
> }
> ```
>
> **`require` для пользовательского ввода - ошибка проектирования.** Некорректный ввод - штатная ситуация, а не баг. Исключение в этом случае заставляет клиентский код оборачивать каждый вызов в `try`/`catch` и сообщает только о первой проблеме.
>
> **Контракт не проверяется компилятором.** Неверный контракт ломает проверки типов - компилятор поверит обещанию и уберёт нужные проверки на `null`:
> ```kotlin
> import kotlin.contracts.ExperimentalContracts
> import kotlin.contracts.contract
>
> @OptIn(ExperimentalContracts::class)
> fun String?.lies(): Boolean {
> contract { returns(true) implies (this@lies != null) }
> return true // врёт: true даже для null
> }
>
> fun main() {
> val s: String? = null
> if (s.lies()) {
> try {
> println(s.length) // компилятор уверен, что s != null
> } catch (e: NullPointerException) {
> println("NPE в non-null коде из-за неверного контракта")
> }
> }
> }
> ```
>
> **Контракт нельзя объявить в методе класса** - только в функции верхнего уровня. Это ограничение текущей реализации.
>
> **Контракты экспериментальны** - `@OptIn(ExperimentalContracts::class)` обязателен, API может измениться.
>
> **`TODO()` компилируется и попадает в релиз.** Он возвращает `Nothing`, поэтому ошибок сборки не вызывает - только падение в рантайме. Перед релизом стоит проверять: `grep -r 'TODO(' src/`.
>
> **Проверки в `init` не спасают `data class` от `copy`** в версиях до Kotlin 2.1: `copy()` вызывает конструктор, то есть `init` выполнится, но саму валидацию обойти через `copy` нельзя - а вот приватный конструктор обойти можно. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
>
> **`require` внутри цикла по большой коллекции стоит дорого** не из-за проверки, а из-за того, что при нарушении создаётся исключение со стектрейсом. Для валидации списков собирайте список ошибок, а не бросайте на первой.
Дополнительно: [Система исключений](Система%20исключений.md)
Далее: [Коллекции](../Коллекции/Коллекции.md)
@@ -0,0 +1,370 @@
---
title: Система исключений
description: Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка
tags:
- kotlin
- exceptions
---
Все исключения в Kotlin **непроверяемые**. Компилятор не требует ни объявлять, ни перехватывать их.
```kotlin
fun parse(s: String): Int = s.toInt() // может бросить, но throws не нужен
fun main() {
println(parse("42")) // 42
try {
parse("abc")
} catch (e: NumberFormatException) {
println("не число: ${e.message}") // не число: For input string: "abc"
}
}
```
## Иерархия
```mermaid
graph TD
T["Throwable"] --> E["Exception"]
T --> R["Error"]
E --> RE["RuntimeException"]
E --> IOE["IOException"]
RE --> NPE["NullPointerException"]
RE --> IAE["IllegalArgumentException"]
RE --> ISE["IllegalStateException"]
RE --> CCE["ClassCastException"]
R --> OOM["OutOfMemoryError"]
R --> SOE["StackOverflowError"]
```
Иерархия та же, что в Java, - это классы JDK. Разница только в том, что Kotlin не делит их на проверяемые и непроверяемые.
| Класс | Смысл | Перехватывать |
|---|---|---|
| `Error` | сбой среды: нет памяти, переполнен стек | нет |
| `RuntimeException` | ошибка программиста: неверный аргумент, состояние | обычно нет, лучше исправить код |
| прочие `Exception` | внешний сбой: сеть, файл, БД | да |
## try как выражение
`try` возвращает значение - это важное отличие от Java:
```kotlin
fun parseOrZero(s: String): Int = try {
s.toInt()
} catch (e: NumberFormatException) {
0
}
fun main() {
println(parseOrZero("42")) // 42
println(parseOrZero("abc")) // 0
}
```
Результат - последнее выражение блока `try` или соответствующего `catch`. Блок `finally` на результат **не влияет**:
```kotlin
fun main() {
val result = try {
"из try"
} finally {
"из finally" // игнорируется
}
println(result) // из try
}
```
## throw - тоже выражение
Тип `throw` - [Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому он подходит в любую позицию:
```kotlin
fun findUser(id: Int): String =
if (id > 0) "пользователь $id"
else throw IllegalArgumentException("неверный id: $id")
fun main() {
println(findUser(1)) // пользователь 1
val name = findUser(1).takeIf { it.isNotEmpty() } ?: throw IllegalStateException("пусто")
println(name) // пользователь 1
}
```
Это позволяет писать `?: throw ...` и `?: error(...)` в одном выражении.
## Перехват
```kotlin
fun risky(mode: Int): String = when (mode) {
1 -> throw IllegalArgumentException("плохой аргумент")
2 -> throw IllegalStateException("плохое состояние")
else -> "ок"
}
fun handle(mode: Int): String = try {
risky(mode)
} catch (e: IllegalArgumentException) {
"аргумент: ${e.message}"
} catch (e: IllegalStateException) {
"состояние: ${e.message}"
} catch (e: Exception) {
"прочее: ${e.message}"
} finally {
println("finally выполняется всегда")
}
fun main() {
println(handle(1)) // аргумент: плохой аргумент
println(handle(3)) // ок
}
```
Блоки `catch` проверяются по порядку, поэтому конкретные типы идут раньше общих. `catch (e: Exception)` первым сделал бы остальные недостижимыми.
Несколько типов в одном блоке Kotlin не поддерживает - аналога Java `catch (A | B e)` нет. Варианты:
```kotlin
fun main() {
try {
throw IllegalStateException("сбой")
} catch (e: RuntimeException) { // общий супертип
println("перехвачено: ${e::class.simpleName}")
}
try {
throw NumberFormatException("плохое число")
} catch (e: Exception) {
when (e) { // или when по типу
is NumberFormatException, is IllegalStateException ->
println("ожидаемая ошибка: ${e.message}")
else -> throw e // остальное пробрасываем
}
}
}
```
## Свои исключения
```kotlin
class InsufficientFundsException(
val required: Long,
val available: Long,
) : Exception("нужно $required, доступно $available")
class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause)
fun withdraw(balance: Long, amount: Long): Long {
if (amount > balance) throw InsufficientFundsException(amount, balance)
return balance - amount
}
fun main() {
println(withdraw(100, 30)) // 70
try {
withdraw(100, 500)
} catch (e: InsufficientFundsException) {
println("не хватает ${e.required - e.available}") // не хватает 400
}
}
```
Своё исключение полезно, когда вызывающему нужно различить ситуацию и получить данные о ней. Для «ошибки программиста» хватает стандартных `IllegalArgumentException` и `IllegalStateException`.
## Цепочка причин
```kotlin
class RepositoryException(message: String, cause: Throwable) : Exception(message, cause)
fun loadUser(id: String): String = try {
"пользователь ${id.toInt()}"
} catch (e: NumberFormatException) {
throw RepositoryException("не удалось загрузить '$id'", e) // сохраняем причину
}
fun main() {
try {
loadUser("abc")
} catch (e: RepositoryException) {
println(e.message) // не удалось загрузить 'abc'
println("причина: ${e.cause?.message}") // причина: For input string: "abc"
}
}
```
Передавать `cause` обязательно: без него теряется исходный стектрейс и причина сбоя.
## use - автоматическое закрытие
Аналог try-with-resources:
```kotlin
import java.io.StringReader
fun main() {
val content = StringReader("данные").use { reader ->
reader.readText() // close() вызовется автоматически
}
println(content) // данные
}
```
`use` - функция-расширение для `Closeable`, она закрывает ресурс и при исключении. Это предпочтительный способ: `finally` с ручным `close()` легко написать неверно.
```kotlin
class Resource(val name: String) : AutoCloseable {
fun work() = "работаю с $name"
override fun close() = println("$name закрыт")
}
fun main() {
val result = Resource("файл").use { it.work() }
println(result)
// файл закрыт
// работаю с файл
try {
Resource("другой").use { throw IllegalStateException("сбой") }
} catch (e: IllegalStateException) {
println("ресурс всё равно закрыт: ${e.message}")
}
}
```
## @Throws для Java
Kotlin не объявляет исключения, но Java-клиенту это может быть нужно:
```kotlin
import java.io.IOException
@Throws(IOException::class)
fun readFile(path: String): String {
if (path.isEmpty()) throw IOException("пустой путь")
return "содержимое $path"
}
fun main() { println(readFile("a.txt")) } // содержимое a.txt
```
Без `@Throws` Java-код не сможет перехватить проверяемое исключение - компилятор Java сочтёт, что метод его не бросает. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
## Чего не делать
```kotlin
fun main() {
// проглатывание: ошибка исчезает
try {
error("сбой")
} catch (e: Exception) {
// ничего — худшая практика
}
// перехват Throwable: ловит OutOfMemoryError и StackOverflowError
try {
error("сбой")
} catch (e: Exception) { // не Throwable
println("перехвачено корректно")
}
// исключение как управление потоком
val items = listOf(1, 2, 3)
println(items.getOrNull(10) ?: "нет элемента") // вместо try/catch
}
```
Правила:
- не перехватывать, если не знаете, что делать с ошибкой - пусть идёт выше
- не перехватывать `Throwable` и `Error`
- не использовать исключения для ожидаемых ситуаций - для них есть `null`, `Result` или sealed-типы
- всегда логировать или пробрасывать; пустой `catch` скрывает причину сбоя
Альтернативы исключениям: [Result и runCatching](Result%20и%20runCatching.md)
## Исключения в корутинах
Отмена корутины реализована через `CancellationException`, и перехватывать её нельзя:
```kotlin
// Требует kotlinx-coroutines-core.
// suspend fun work() {
// try {
// delay(1000)
// } catch (e: CancellationException) {
// throw e // обязательно пробросить!
// } catch (e: Exception) {
// println("настоящая ошибка")
// }
// }
```
`catch (e: Exception)` перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | checked exceptions | **нет**, все непроверяемые |
> | `throws IOException` в сигнатуре | `@Throws` только для Java-клиентов |
> | `try` - инструкция | `try` - выражение |
> | `catch (A \| B e)` | нет аналога, нужен `when` |
> | try-with-resources | `use { }` |
> | `throw` - инструкция | `throw` - выражение типа `Nothing` |
>
> **Отсутствие checked exceptions - самое спорное решение.** Аргументы против них, которые приводят авторы Kotlin:
>
> 1. **Они не масштабируются.** Сигнатура с пятью `throws` нечитаема, а обобщённый код (`map`, `forEach`) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями.
> 2. **На практике их проглатывают.** Самый частый способ «обработать» обязательное исключение - пустой `catch`, то есть защита превращается в сокрытие ошибки.
> 3. **Обёртывание ради компиляции.** Код вроде `catch (IOException e) { throw new RuntimeException(e); }` встречается в любом крупном Java-проекте.
>
> Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде `Result`, но компилятор уже не помогает.
>
> **`try` как выражение убирает `var`:**
> ```java
> int value; // Java: нужна изменяемая переменная
> try { value = Integer.parseInt(s); }
> catch (NumberFormatException e) { value = 0; }
> ```
> ```kotlin
> fun parseOrZero(s: String): Int =
> try { s.toInt() } catch (e: NumberFormatException) { 0 } // val вместо var
>
> fun main() { println(parseOrZero("abc")) } // 0
> ```
>
> **`use` вместо try-with-resources** - обычная функция, а не синтаксис. Поэтому её можно применить к своему типу, добавив реализацию `Closeable`.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **Сигнатура не говорит об исключениях.** Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc:
> ```kotlin
> /**
> * @throws IllegalArgumentException если id не положительный
> */
> fun load(id: Int): String {
> require(id > 0) { "id должен быть положительным" }
> return "данные $id"
> }
>
> fun main() { println(load(1)) } // данные 1
> ```
>
> **`finally` не влияет на результат `try`-выражения** - значение из него отбрасывается.
>
> **`return` в `finally` проглатывает исключение:**
> ```kotlin
> fun bad(): String {
> try {
> throw IllegalStateException("потеряется")
> } finally {
> return "из finally" // исключение исчезло!
> }
> }
>
> fun main() { println(bad()) } // из finally
> ```
> Исключение не дойдёт до вызывающего. `return` в `finally` писать нельзя.
>
> **Перехват `Exception` ловит все `RuntimeException`,** включая `NullPointerException` в вашем коде - то есть маскирует собственные баги под «обработанную ошибку».
>
> **`CancellationException` нельзя проглатывать в корутинах** - иначе отмена перестанет работать.
>
> **Исключение в `init` или в инициализаторе свойства оставляет побочные эффекты.** Объект не создастся, но то, что уже выполнилось, останется. Подробнее: [Инициализация](../ООП/Инициализация.md)
>
> **Создание исключения дорого** из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте `null` или `Result`:
> ```kotlin
> fun main() {
> println("abc".toIntOrNull() ?: 0) // без исключения
> // println("abc".toInt()) // исключение + стектрейс
> }
> ```
>
> **Исключение из лямбды в `forEach` прерывает весь цикл**, а не одну итерацию:
> ```kotlin
> fun main() {
> val results = mutableListOf<Int>()
> try {
> listOf("1", "abc", "3").forEach { results.add(it.toInt()) }
> } catch (e: NumberFormatException) {
> println("прервано на второй строке, собрано: $results") // [1]
> }
> }
> ```
> Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте `mapNotNull { it.toIntOrNull() }`.
>
> **`@Throws` не проверяется компилятором Kotlin** - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот.
Далее: [Result и runCatching](Result%20и%20runCatching.md)