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 -> "негативне"
        x == 0 -> "нуль"
        else -> "позитивне"
    }

    println(sign) // нуль
}

Підводний камінь: 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 дозволяє не покривати всі випадки. Якщо нічого не підійшло — не станеться нічого. Для командного інтерфейсу це майже завжди поганий користувацький досвід: людина ввела команду й не зрозуміла, що сталося. Тому під час оброблення команд 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
Опитування
Оператор <code><span class="text-user">when</span></code>, рівень 13, лекція 5
Недоступний
Оператор when
when: розгалуження, валідація, команди
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ