JavaRush /Курсы /Kotlin SELF /Предусловия и проверка состояния: require и check

Предусловия и проверка состояния: require и check

Kotlin SELF
31 уровень , 4 лекция
Открыта

1. Fail-fast и разница между require и check

Зачем вообще нужно «падать раньше»: идея fail-fast

Если вы только начинаете, может казаться, что хорошая программа должна быть вежливой и терпеливой: «ну пользователь ввёл ерунду — давайте тихо подставим 0, пустую строку и как-нибудь дальше». На практике такая «доброта» часто превращается в накопление ошибок: программа продолжает работать, но данные становятся мусорными, и в итоге ломается всё… только гораздо позже и гораздо непонятнее.

Подход fail-fast (буквально «упади быстро») говорит: если дальше логика не имеет смысла, лучше остановиться сразу и объяснить, что именно было не так. Kotlin прямо поддерживает этот стиль стандартными функциями-предусловиями: они автоматически выбрасывают исключения с понятным типом.

Чтобы картинка сложилась, держите маленькую схему (это именно «про смысл», не про синтаксис):

flowchart TD
    A[Получили данные] --> B{Данные пригодны?}
    B -- Нет --> C[Fail-fast: require/check -> исключение]
    B -- Да --> D[Продолжаем логику]
    D --> E{Состояние корректно?}
    E -- Нет --> F[Fail-fast: check -> исключение]
    E -- Да --> G[Результат предсказуем]

Самый главный бонус fail-fast в учебном проекте: вы перестаёте гадать «почему оно странно считает», и начинаете получать ответы уровня «Age must be >= 0, got -5». Удивительно, насколько это снижает желание швырнуть ноутбук в окно (я проверял, окно ни в чём не виновато).

require vs check: одно похоже, но смысл разный

Снаружи require(condition) и check(condition) выглядят почти одинаково: вы передаёте условие и (обычно) сообщение. Но смысл у них разный — и это важно, потому что смысл превращается в тип исключения, а тип исключения — это подсказка для вас и для того, кто будет читать код после вас.

require() используют для проверки корректности входных аргументов, а check() — для проверки корректности состояния объекта или переменной. При нарушении require() вылетает IllegalArgumentException, а при нарушении check()IllegalStateException.

Сведём в таблицу (её реально стоит держать в голове):

Вопрос, который вы задаёте Что использовать Типичная мысль Исключение
«Мне передали плохие данные, я не могу продолжать»
require(...)
«Аргумент неправильный»
IllegalArgumentException
«Данные-то нормальные, но объект сейчас в невозможном/запрещённом состоянии»
check(...)
«Состояние неправильное»
IllegalStateException

Очень грубая, но запоминающаяся аналогия: require — это фейс-контроль на входе в клуб («в кедах нельзя»), а check — охрана внутри («в бассейн нельзя с тостером»). И если вам кажется, что «и там и там нельзя», вы правы. Но причины «нельзя» разные.

2. require: защищаем вход

Когда вы создаёте объект, вы хотите, чтобы он появлялся сразу корректным. Это прям ключевая идея ООП на уровне новичка: если объект существует, ему можно доверять. В Kotlin типичный способ сделать это — поставить проверки в init { ... } или прямо в конструкторной логике.

Минимальный пример require в init

Начнём с чего-то максимально простого, чтобы рука привыкла:

class Age(val value: Int) {
    init {
        require(value >= 0) { "Age must be >= 0, got $value" }
    }
}

fun main() {
    println(Age(10).value) // 10
}

Здесь мы зафиксировали правило: возраст не может быть отрицательным. Важно, что мы не «чинить» пытаемся, а запрещаем создание некорректного объекта. И это нормально: если вам прилетело -10, это не «почти возраст», это ошибка входа.

Пример из учебного проекта: трекер расходов

Продолжим практический пример: консольный трекер расходов. Раньше мы могли хранить расход как набор полей, а теперь хотим, чтобы объект Expense не мог существовать в мусорном виде.

Сделаем небольшой класс:

class Expense(
    val amountCents: Int,
    val category: String,
    val note: String
) {
    init {
        require(amountCents > 0) { "amountCents must be > 0, got $amountCents" }
        require(category.isNotBlank()) { "category must not be blank" }
        require(note.length <= 100) { "note must be <= 100 chars, got ${note.length}" }
    }
}

Обратите внимание на стиль: сообщение должно объяснять, что ожидали и что получили. Это не эстетика — это скорость отладки.

Теперь создание:

fun main() {
    val e = Expense(amountCents = 1999, category = "Food", note = "Pizza")
    println("${e.category}: ${e.amountCents} cents") // Food: 1999 cents
}

Если кто-то попытается создать Expense(amountCents = 0, ...), объект не создастся — и это правильно: нулевой расход чаще всего означает, что мы где-то не распарсили число или перепутали валюту.

require подходит и для обычных методов

Предусловия нужны не только при создании объекта. Если у вас есть метод, который принимает аргументы извне, вы тоже можете защищать вход require-ом: это как раз про аргументы, без которых функция не может работать.

Представим, что у нас есть сервисный метод форматирования суммы:

fun formatCents(amountCents: Int): String {
    require(amountCents >= 0) { "amountCents must be >= 0, got $amountCents" }
    val dollars = amountCents / 100
    val cents = amountCents % 100
    return "$dollars.${cents.toString().padStart(2, '0')}"
}

fun main() {
    println(formatCents(1999)) // 19.99
}

Да, можно было бы «молча» превратить -5 в 0. Но тогда вы потеряете сигнал о том, что где-то у вас ошибка.

3. check: защищаем состояние объекта

Теперь перейдём к check. Он нужен тогда, когда входные данные нормальные, но сам объект сейчас не должен выполнять операцию. Это часто встречается в объектах с «режимом работы»: сессия может быть закрыта, файл — уже сохранён и «sealed», отчёт — уже финализирован.

check() — это про проверку корректности состояния объекта или переменной, и при провале он выбрасывает IllegalStateException.

Пример: «закрытый» трекер расходов

Добавим в наш проект класс, который хранит список расходов. Мы ещё не обсуждали коллекции объектов глубоко, поэтому сделаем максимально просто: список плюс флаг closed.

class ExpenseTracker {
    private val expenses: MutableList<Expense> = mutableListOf()
    private var closed: Boolean = false

    fun close() {
        check(!closed) { "Tracker already closed" }
        closed = true
    }

    fun add(expense: Expense) {
        check(!closed) { "Can't add expense: tracker is closed" }
        expenses.add(expense)
    }

    fun totalCents(): Int {
        var sum = 0
        for (e in expenses) sum += e.amountCents
        return sum
    }
}

Смотрите, что мы сделали: вход в add(expense) как таковой корректный (объект Expense уже гарантированно валиден). Но добавлять его нельзя, если трекер закрыт. Это и есть проверка состояния.

Как это выглядит при использовании

fun main() {
    val tracker = ExpenseTracker()
    tracker.add(Expense(500, "Coffee", "Latte"))
    tracker.close()

    println(tracker.totalCents()) // 500

    // tracker.add(Expense(100, "Snack", "Bar"))
    // Если раскомментировать: IllegalStateException из check(...)
}

Почему это хорошо: ошибка сообщает вам, что проблема не в расходе и не в «вводе», а в жизненном цикле объекта. Такой баг обычно лечится не «подставь другое число», а «не вызывай метод в этом состоянии».

4. Сообщения и обработка исключений

Сообщения в require/check: как сделать исключение полезным

Когда начинающие ставят require(x > 0) без сообщения, они рассчитывают, что «и так понятно». Но через две недели (или через два часа, если день был тяжёлый) вы увидите в консоли что-то вроде «Failed requirement.» и будете вспоминать, какой именно requirement и где именно он failed.

Kotlin позволяет писать сообщение как строку или как лямбду { ... }. Лямбда удобна тем, что вычисляется только если проверка провалилась (то есть в нормальном случае не тратит ресурсы).

Хорошее сообщение — это почти всегда формат: ожидали → получили.

fun parseAmountCents(text: String): Int {
    val trimmed = text.trim()
    val value = trimmed.toIntOrNull()
    require(value != null) { "Amount must be an integer, got '$text'" }
    require(value > 0) { "Amount must be > 0, got $value" }
    return value
}

fun main() {
    println(parseAmountCents(" 150 ")) // 150
}

Тут две разные ошибки: не число и число, но плохое. И сообщения разные — это экономит вам нервы при отладке.

А для check сообщение должно объяснять «почему состояние запрещено»:

class Session {
    private var started: Boolean = false

    fun start() {
        check(!started) { "Session already started" }
        started = true
    }
}

Это почти идеальный минимальный пример: аргументов нет, а операция запрещена из-за состояния.

Как аккуратно подружить require/check с консольным main

Fail-fast хорош внутри вашей модели, но консольное приложение не обязано падать «красивым стектрейсом» от первой ошибки пользователя. Поэтому часто делают так: внутри классов — require/check, а на уровне main — аккуратный перехват и понятное сообщение человеку.

Это не противоречие. Это разделение обязанностей: модель говорит «так нельзя», а CLI решает «как объяснить это пользователю».

Соберём маленький пример: пользователь вводит сумму, категорию, заметку, а мы добавляем расход.

fun main() {
    val tracker = ExpenseTracker()

    try {
        print("Amount (cents): ")
        val amount = parseAmountCents(readln())

        print("Category: ")
        val category = readln()

        print("Note: ")
        val note = readln()

        tracker.add(Expense(amount, category, note))
        println("Added!") // Added!
        println("Total = ${tracker.totalCents()} cents") // Total = ... cents
    } catch (e: IllegalArgumentException) {
        println("Input error: ${e.message}")
    } catch (e: IllegalStateException) {
        println("App state error: ${e.message}")
    }
}

Здесь видно пользу разных исключений: IllegalArgumentException мы трактуем как проблему ввода, а IllegalStateException — как проблему порядка действий. Это ровно то смысловое разделение, ради которого require и check вообще существуют.

5. Типичные ошибки при использовании require/check

Ошибка №1: использовать check для пользовательского ввода.
Такое часто происходит по привычке: «ну я же проверяю условие — значит check». Но смысл у check другой: он про ситуацию «мы сами довели объект до невозможного состояния». Если пользователь ввёл -5, это не «состояние объекта плохое», это «аргумент плохой», то есть require. Kotlin как раз разделяет эти случаи и даже выбрасывает разные исключения: require даёт IllegalArgumentException, а check — IllegalStateException.

Ошибка №2: делать сообщения бесполезными («bad input», «error», «oops»).
Когда вы пишете сообщение в стиле «wrong value», вы выигрываете ровно ноль секунд. Сообщение должно быть диагностикой: что ожидалось и что пришло. Идеальный формат: «X must be …, got …». Особенно полезно добавлять фактическое значение и длину строки, если проверяете текст.

Ошибка №3: ставить проверки слишком поздно, после изменения состояния.
Например, метод успел добавить расход в список, потом проверил, что трекер закрыт, и упал. В итоге упал, но данные уже частично изменены. Поэтому проверки require/check почти всегда ставят в начале метода — до любых изменений. Это делает код предсказуемым: либо операция полностью выполнена, либо не начата.

Ошибка №4: пытаться «починить всё» нормализацией, где нужна жёсткая валидация.
Иногда хочется заменить require(amount > 0) на «если меньше нуля — сделаем ноль». Но это превращает ошибки в тихие баги. Нормализация полезна для вещей вроде trim() и приведения регистра, но для ключевых чисел и идентификаторов чаще безопаснее fail-fast: не создавать объект, если он по смыслу не может существовать.

Ошибка №5: хранить в объекте некорректное состояние и надеяться «потом поправим».
Если ваш Expense может существовать с пустой категорией, то рано или поздно вы забудете его поправить, и он доживёт до отчёта, сохранения или сортировки. Правило «создали — значит корректен» сильно упрощает мышление о коде. Поэтому проверки в init — не занудство, а инвестиция в спокойный сон.

1
Задача
Kotlin SELF, 31 уровень, 4 лекция
Недоступна
Возраст валидация
Возраст валидация
1
Задача
Kotlin SELF, 31 уровень, 4 лекция
Недоступна
Копейки в чек
Копейки в чек
1
Задача
Kotlin SELF, 31 уровень, 4 лекция
Недоступна
Счёт в банке
Счёт в банке
1
Задача
Kotlin SELF, 31 уровень, 4 лекция
Недоступна
Конструктор отчёта
Конструктор отчёта
1
Опрос
Конструкторы и `init`, 31 уровень, 4 лекция
Недоступен
Конструкторы и `init`
Конструкторы и `init`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ