1. Коли when стає проблемою
Якщо ви зараз думаєте: «У мене ж when працює — навіщо ще говорити про стиль?», то ви вже на правильному шляху. У програмуванні стиль — це не про «естетичну красу заради краси», а про передбачуваність. Код ви пишете один раз, а читаєте (і виправляєте) його багато разів — іноді в стані «я не виспався, кава закінчилася, а воно падає».
when добрий тим, що читачеві одразу видно: «ось варіанти, ось дії». Але у when є три характерні «болячки», які в початківців трапляються майже завжди:
- гілки починають перетинатися, і логіка залежить від порядку рядків (це прихована пастка);
- else або забувають, або використовують як «смітник» на всяк випадок;
- гілки розростаються, і 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 працювати з уже чистими значеннями.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ