JavaRush /Курсы /Kotlin SELF /Ошибки и стиль when

Ошибки и стиль when

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

1. Когда when становится проблемой

Если вы сейчас думаете: «У меня же when работает — зачем ещё говорить о стиле?», то вы уже на правильном пути. В программировании стиль — это не про эстетическую красоту ради красоты, а про предсказуемость. Вы пишете код один раз, а читаете (и чините) его много раз, причём иногда в состоянии «я не выспался, кофе закончился, и оно падает».

when хорош тем, что читателю видно: «вот варианты, вот действия». Но у when есть три характерные «болячки», которые встречаются у начинающих почти всегда:

  1. ветки начинают пересекаться, и логика зависит от порядка строк (это скрытая ловушка);
  2. else либо забывают, либо используют как мусорное ведро «на всякий случай»;
  3. ветки разрастаются, и when перестаёт быть таблицей — превращается в мини-программу внутри программы.

Чтобы лечить эти болячки, важно помнить базовое правило: when проверяет ветки сверху вниз и срабатывает первая подходящая. Это не «поиск лучшего совпадения», а «первый, кто подошёл — тот и победил».

2. Пересечения условий: когда порядок строк становится логикой

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

Классическая ловушка: широкий диапазон «перехватывает» узкий

Сначала сделаем маленький пример «как не надо». Он компилируется, запускается, но логика обманывает нас.

fun main() {
    val score = 42

    val label = when (score) {
        in 0..100 -> "В пределах нормы"
        in 0..50 -> "Скорее низкий"
        else -> "Некорректно"
    }

    println(label) // В пределах нормы
}

Если вы ожидали «Скорее низкий», то это нормально: вы мыслите по-человечески («42 входит и туда, и туда, но 0..50 как будто точнее»). А when мыслит по-машинному: первая ветка уже подошла — значит остальные даже не проверяем.

Правильный фикс не волшебный: мы просто ставим более узкое условие выше.

fun main() {
    val score = 42

    val label = when (score) {
        in 0..50 -> "Скорее низкий"
        in 51..100 -> "В пределах нормы"
        else -> "Некорректно"
    }

    println(label) // Скорее низкий
}

Здесь мы сделали диапазоны непересекающимися, и порядок веток перестал быть скрытой логикой.

«Защитная» ветка !in: полезно, но требует дисциплины

Валидация через !in выглядит красиво: сначала отсекаем явно неверное, потом классифицируем остальное. Это реально хороший паттерн — просто надо понимать, что !in обычно широкое условие, и оно должно стоять там, где не ломает остальные проверки.

fun main() {
    val temperature = 999

    val message = when (temperature) {
        !in -50..60 -> "Вне разумного диапазона"
        in -50..0 -> "Холодно"
        in 1..25 -> "Нормально"
        else -> "Жарко"
    }

    println(message) // Вне разумного диапазона
}

Здесь всё хорошо: !in -50..60 не пересекается с остальными ветками, потому что остальные ветки целиком внутри диапазона -50..60.

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

Мини-схема: как работает порядок веток

Иногда помогает буквально нарисовать, что происходит:

flowchart TD
    A[Значение x] --> B{Первая ветка подходит?}
    B -- да --> C[Выполняем первую ветку]
    B -- нет --> D{Вторая ветка подходит?}
    D -- да --> E[Выполняем вторую ветку]
    D -- нет --> F{...}
    F -- нет --> G["else (если есть)"]

Схема тривиальная, но она объясняет 90% «почему оно так сработало».

3. else, исчерпываемость и «молчаливые» when

else — это не «обязательная заплатка», а способ явно сказать: «я предусмотрел всё остальное». Но нюанс в том, что требования к else зависят от того, используете ли вы when как выражение или как оператор. И вот здесь начинаются типовые ошибки: где-то компилятор ругается, а где-то — молчит, и программа «ничего не делает».

Kotlin прямо разделяет when на statement и expression, и у expression есть требование исчерпываемости: нужно вернуть значение для любого входа.

Ошибка «оно ничего не сделало» (когда when — оператор)

Если when используется как оператор (то есть мы не присваиваем результат в переменную), Kotlin разрешает не покрывать все случаи. Это может быть удобно, но для новичка это часто ловушка: «я думал, что оно хотя бы скажет, что команда неизвестна».

fun main() {
    val cmd = "unknown"

    when (cmd) {
        "help" -> println("Справка")
        "exit" -> println("Выход")
    }

    println("После when") // После when
}

Программа «молча» пропустила when. Ошибки нет. Иногда это нормально. Но для командного интерфейса почти всегда плохая идея: пользователь вводит команду, а в ответ — тишина. Поэтому в обработке команд else — это не опция, а элемент вежливости.

fun main() {
    val cmd = "unknown"

    when (cmd) {
        "help" -> println("Справка")
        "exit" -> println("Выход")
        else -> println("Неизвестная команда: $cmd") // Неизвестная команда: unknown
    }
}

Ошибка «не компилируется» (когда when — выражение)

Когда when возвращает значение, компилятор требует исчерпываемости — иначе он не может гарантировать, что переменная получит значение. Это не «придирка Kotlin», это реально защита от дыр в логике.

fun main() {
    val cmd = "help"

    // Так нельзя: нет else, а значит не все случаи покрыты
    // val message = when (cmd) {
    //     "help" -> "Справка"
    // }

    val message = when (cmd) {
        "help" -> "Справка"
        else -> "Неизвестная команда: $cmd"
    }

    println(message) // Справка
}

else как диагностика, а не как «всё равно что»

Очень частая ошибка — сделать else -> "" или else -> 0, чтобы «компилятор отстал». Формально это работает, но вы сами выключаете себе диагностику. В командах else должен помогать понять, что пошло не так: печатать исходное значение, говорить, что ожидалось, и не скрывать проблему.

4. Длинные ветки: как не превратить when в мини-программу

Когда вы только учитесь, есть соблазн написать всё в одной ветке: и распарсить число, и проверить диапазон, и вывести три строки, и обновить состояние. Это нормально как первый шаг, но как только ветка становится длиннее 10–15 строк, when перестаёт выполнять свою главную работу — быть таблицей вариантов.

Kotlin-стиль (и здравый смысл) обычно советует предпочитать выраженческую форму when, if, try там, где это делает код короче и яснее. Но «короче» — не значит «в одну гигантскую ветку».

Плохой стиль: when делает всё сразу

Представим команду grade <число>, которая печатает оценку по баллам. Вот пример, который работает, но читается тяжело.

fun main() {
    val arg = "72"
    val points = arg.toIntOrNull()

    when (points) {
        null -> println("Нужно число")
        else -> {
            if (points < 0 || points > 100) {
                println("Нужно 0..100")
            } else if (points >= 90) {
                println("A")
            } else if (points >= 80) {
                println("B")
            } else {
                println("C или ниже")
            }
        }
    }
}

Здесь смешались три уровня: парсинг, валидация и бизнес-логика. В маленьком примере это терпимо, но в реальном приложении ветка быстро раздувается.

Лучше: when возвращает результат, а печать — отдельно

Сделаем две идеи: во-первых, when возвращает строку; во-вторых, внутри when мы не плодим вложенные if, если можно сделать это аккуратнее.

fun gradeMessage(points: Int?): String {
    return when (points) {
        null -> "Нужно число"
        !in 0..100 -> "Нужно 0..100"
        in 90..100 -> "Оценка: A"
        in 80..89 -> "Оценка: B"
        else -> "Оценка: C или ниже"
    }
}

fun main() {
    val points = "72".toIntOrNull()
    val message = gradeMessage(points)

    println(message) // Оценка: C или ниже
}

Мы выиграли сразу в нескольких местах. when стал таблицей. Ветки короткие. Печать одна. Логика стала тестируемой даже «в голове»: вы легко пробегаете глазами ветки и видите покрытие.

Технический нюанс: ветка с блоком — это нормально, но блок должен быть маленьким

Иногда в ветке правда нужно сделать два действия. Это допустимо. Просто держите блок компактным, и если внутри уже появляется ещё один when или три if — это сигнал вынести кусок в функцию.

fun main() {
    val cmd = "help"

    when (cmd) {
        "help" -> {
            println("Команды: help, grade <0..100>, exit") // Команды: help, grade <0..100>, exit
            println("Пример: grade 90")                    // Пример: grade 90
        }
        else -> println("Неизвестная команда: $cmd")
    }
}

5. Выбор формы: if vs when (value) vs when { ... }

Эта часть кажется вкусовщиной, но на практике это прям инструмент против ошибок. Если вы выбираете форму ветвления по смыслу, код получается проще, а пересечения условий встречаются реже. Kotlin-конвенции прямо рекомендуют: для бинарных условий чаще лучше if, а when — когда вариантов три и больше.

if — когда реально два варианта

Если у вас ровно два пути, if обычно читабельнее. Не потому что when «плох», а потому что if в голове читается как «либо так, либо иначе».

fun main() {
    val raw = "   "

    val input: String? = if (raw.trim().isEmpty()) null else raw.trim()
    println(input) // null
}

Можно сделать и when, но это будет выглядеть как «таблица из двух строк», что обычно избыточно.

when (value) — когда сравниваем с конкретными значениями

Команды, коды, статусы — это прям «родная территория» when (value): читается как таблица соответствия.

fun main() {
    val role = "editor"

    val access = when (role) {
        "viewer" -> "Только чтение"
        "editor" -> "Можно править"
        "admin" -> "Можно всё (и даже случайно удалить мир)"
        else -> "Неизвестная роль: $role"
    }

    println(access) // Можно править
}

when { ... } — когда ветки это условия, а не значения

Форма when { ... } — отличная замена цепочке if/else if, когда вы классифицируете по условиям. В этой форме else обязателен: без него компилятор не даст собрать код, потому что иначе непонятно, что делать, если ни одно условие не true.

fun main() {
    val x = 0

    val sign = when {
        x < 0 -> "negative"
        x == 0 -> "zero"
        else -> "positive"
    }

    println(sign) // zero
}

Подводный камень: when { ... } проще «сломать» пересечениями

Когда условия булевы, пересечения ещё легче сделать случайно. Например, если вы сначала написали x >= 0, а потом x == 0, то ветка x == 0 никогда не сработает.

fun main() {
    val x = 0

    val label = when {
        x >= 0 -> "Не отрицательное"
        x == 0 -> "Ноль"
        else -> "Отрицательное"
    }

    println(label) // Не отрицательное
}

Фикс снова простой: более узкое выше или условия сделать взаимоисключающими.

6. Стиль командного when: аккуратная таблица команд

Сейчас мы соберём всё в тот самый «командный диспетчер», который вы начали делать в предыдущей лекции. Наша цель — не добавить новые фичи, а показать стиль: нормализация до when, ветки короткие, неизвестные случаи обрабатываются явно, а пересечения не прячутся внутри порядка строк.

Шаг 1: нормализуем и делим ввод на command и arg

Сделаем маленькую функцию, которая принимает уже обрезанную строку и возвращает пару: команда и аргумент. Мы используем только знакомые инструменты indexOf и substring.

fun splitCommand(line: String): Pair<String, String> {
    val spaceIndex = line.indexOf(' ')
    val command = if (spaceIndex == -1) line else line.substring(0, spaceIndex)
    val arg = if (spaceIndex == -1) "" else line.substring(spaceIndex + 1).trim()
    return command.lowercase() to arg
}

Здесь важно: splitCommand не читает ввод и не печатает. Она просто преобразует строку. Такой кусок легко переиспользовать и сложно «сломать случайно».

Шаг 2: when как выражение, возвращающее ответ

Теперь сделаем обработчик, который возвращает строку-ответ. Это уменьшает соблазн печатать в каждой ветке, а ещё помогает избегать веток-простыней: ветка должна сводиться к вычислению ответа.

fun handleCommand(command: String, arg: String): String {
    return when (command) {
        "help", "h", "?" -> "Команды: help, grade <0..100>, echo <text>, exit"
        "echo" -> arg
        "grade" -> gradeMessage(arg.toIntOrNull())
        "exit" -> "EXIT"
        else -> "Неизвестная команда: $command"
    }
}

Обратите внимание на стиль: мы сгруппировали синонимы команды через запятую, чтобы не дублировать одинаковые действия. Такой синтаксис — одна из причин, почему when часто читабельнее цепочки if/else if.

Шаг 3: main становится простым «конвейером»

Остаётся собрать маленький цикл. Мы уже умеем while, поэтому всё честно.

fun main() {
    while (true) {
        print("> ")
        val raw = readln().trim()

        if (raw.isEmpty()) {
            println("Введите команду. Для справки: help") // Введите команду. Для справки: help
            continue
        }

        val (command, arg) = splitCommand(raw)
        val response = handleCommand(command, arg)

        if (response == "EXIT") break
        println(response)
    }
}

Это хороший пример того, ради чего мы вообще говорим про стиль when. У вас появляется «слоёный пирог» (в хорошем смысле): ввод → подготовка → разбор → диспетчеризация → вывод. И каждое место делает одну работу.

Небольшая схема пайплайна команд

Иногда полезно видеть это как линию сборки:

flowchart LR
    A["readln()"] --> B["trim()"]
    B --> C{isEmpty?}
    C -- да --> D[сообщение + continue]
    C -- нет --> E[splitCommand]
    E --> F[handleCommand with when]
    F --> G[println/exit]

И да: если вы почувствовали себя инженером на заводе — поздравляю, вы только что сделали первый шаг в сторону архитектуры.

Когда when начинает разрастаться

В жизни команд становится больше. Ветки растут. И однажды ваш красивый when (command) превращается в полотно на 80 строк, где половина веток ещё и повторяет одно и то же. Это не «плохо», это естественный рост. Важно вовремя заметить момент, когда when перестал быть таблицей.

Признаки обычно такие: в ветке появляются вложенные if, повторяющиеся парсинги arg.toIntOrNull(), одинаковые сообщения об ошибках, и вы начинаете прокручивать when колёсиком мыши.

Самый простой способ сдержать рост — не пытаться «победить» when, а помочь ему: выносить повторяющиеся куски в функции и заранее готовить данные до диспетчеризации. Например, если половина команд работает с числом, можно сделать одну функцию парсинга аргумента, которая возвращает Int?, и уже её результат использовать в when. Это не новая тема — это обычная декомпозиция, которую вы уже делали на функциях.

7. Типичные ошибки при работе со стилем when

Ошибка №1: пересекающиеся условия, где порядок веток становится скрытой логикой.
Самая коварная ситуация — когда код компилируется и «часто работает», но ветка ниже никогда не срабатывает, потому что выше стоит более широкий диапазон или условие. Это лечится либо перестановкой веток (узкое выше широкого), либо тем, что вы делаете условия взаимоисключающими и перестаёте зависеть от порядка.

Ошибка №2: else используется как «мусорка», чтобы компилятор отстал.
Когда when — выражение, компилятор требует исчерпываемости. Новички иногда ставят else -> "" или else -> 0. Формально программа собирается, но вы теряете диагностику: ошибка превращается в тихий неверный результат. Лучше делать else осмысленным: сообщать о неизвестном значении, показывать исходный ввод.

Ошибка №3: when без else в командах превращается в «тишину» для пользователя.
Когда when используется как оператор, Kotlin позволяет не покрывать все случаи, и если ничего не подошло — не произойдёт ничего. Для командного интерфейса это почти всегда плохой UX: человек ввёл команду и не понял, что случилось. Поэтому в обработке команд else — обязательная ветка «неизвестная команда».

Ошибка №4: ветки становятся слишком длинными, и when перестаёт быть таблицей.
Как только внутри ветки появляются парсинг, валидация, несколько println, вложенные if и ещё один when, читабельность резко падает. Правильная привычка — возвращать из when результат (строку/статус), а детали выносить в функции. Это делает код линейным и помогает удерживать ветки короткими.

Ошибка №5: выбирают when там, где проще if, и наоборот.
Если вариантов ровно два, if обычно читается быстрее. Kotlin-конвенции прямо советуют предпочитать if для бинарных условий, а when — для трёх и более вариантов. Обратная ошибка — писать длинную цепочку if/else if там, где по смыслу это «таблица значений»: там when (value) делает код ровнее и уменьшает дублирование.

Ошибка №6: нормализацию ввода делают внутри веток, а не до when.
Если вы в каждой ветке пишете raw.trim().lowercase() или делаете substring() прямо в условиях when, то ваш when перестаёт быть диспетчером: он становится «местом, где мы делаем всё подряд». Гораздо надёжнее один раз посчитать command и arg, а в when работать с уже чистыми значениями.

1
Задача
Kotlin SELF, 13 уровень, 5 лекция
Недоступна
Ранг по очкам
Ранг по очкам
1
Задача
Kotlin SELF, 13 уровень, 5 лекция
Недоступна
Вежливый бот
Вежливый бот
1
Задача
Kotlin SELF, 13 уровень, 5 лекция
Недоступна
Тариф доставки
Тариф доставки
1
Задача
Kotlin SELF, 13 уровень, 5 лекция
Недоступна
Статус заказа
Статус заказа
1
Опрос
Оператор <code><span class="text-user">when</span></code>, 13 уровень, 5 лекция
Недоступен
Оператор when
when: ветвления, валидация, команды
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ