--- 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): 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 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 = buildList { if ("@" !in email) add("email без @") if (age !in 0..150) add("недопустимый возраст: $age") } fun create(email: String, age: Int): Result { 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)