JavaRush /Курси /Kotlin SELF /when з enum — вичерпні гілки і when як вираз

when з enum — вичерпні гілки і when як вираз

Kotlin SELF
Рівень 34 , Лекція 1
Відкрита

1. Вступ

Коли ви починаєте використовувати enum у реальному коді, дуже швидко виникає цілком практичне питання: «Гаразд, у мене є ExpenseCategory.FOOD. Що з цим робити далі?» Показати гарну назву? Вибрати ліміт? Підібрати підказку для користувача? Вивести іконку текстом? І саме тут люди часто зʼїжджають у if або в «магічні рядки». when — це якраз інструмент, який перетворює обробку варіантів enum на зрозумілу таблицю відповідностей.

Уявіть, що enum — це набір кнопок на пульті. Кнопок небагато, вони підписані, а їхній список відомий заздалегідь. when — це ваша інструкція: «якщо натиснута така кнопка — роби ось це». А якщо згодом ви додасте нову кнопку, Kotlin зможе нагадати: «Гей, інструкцію для нової кнопки ви не написали».

2. Згадуємо when: оператор і вираз

На словах різниця звучить занудно: оператор — «просто виконує код», вираз — «повертає значення». Але на практиці саме це визначає, наскільки коротким і читабельним буде ваш код. Коли when використовується як вираз, ми часто позбуваємося тимчасових змінних і дублювання return. Код стає схожим на акуратну табличку: «ось вхід → ось вихід».

У Kotlin when можна писати і так, і так. І найприємніше: when-вираз можна присвоїти змінній або повернути з функції. Для enum це особливо природно: вхід — один із фіксованих варіантів, вихід — наприклад, текст для інтерфейсу або число для розрахунку.

Мініприклад: when як оператор

enum class Status { NEW, IN_PROGRESS, DONE }

fun printStatus(status: Status) {
    when (status) {
        Status.NEW -> println("Створено")              // Створено
        Status.IN_PROGRESS -> println("У роботі")     // У роботі
        Status.DONE -> println("Готово")              // Готово
    }
}

Тут when просто виконує println() у потрібній гілці.

Мініприклад: when як вираз

enum class Status { NEW, IN_PROGRESS, DONE }

fun statusLabel(status: Status): String =
    when (status) {
        Status.NEW -> "Створено"
        Status.IN_PROGRESS -> "У роботі"
        Status.DONE -> "Готово"
    }

Зверніть увагу: гілки повертають рядки, і весь when також повертає рядок. Жодних var label = ... і трьох return.

3. Вичерпний when за enum

Зараз буде один із найприємніших моментів у Kotlin: коли when працює з enum, компілятор знає повний список варіантів. А отже, може перевірити, що ви обробили кожен із них. Це називається вичерпний when (exhaustive).

Психологічно це відчувається як «я не забув якийсь випадок». Технічно — як захист від майбутніх змін. Якщо ви додасте новий елемент enum, Kotlin підсвітить місця, де обробка стала неповною.

Зробімо невеликий фрагмент для нашого навчального консольного застосунку (умовно «облік витрат»). Нехай у нас є категорії витрат:

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

І ми хочемо гарно вивести назву категорії українською.

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

fun categoryTitle(category: ExpenseCategory): String =
    when (category) {
        ExpenseCategory.FOOD -> "Їжа"
        ExpenseCategory.TRANSPORT -> "Транспорт"
        ExpenseCategory.HOME -> "Дім"
        ExpenseCategory.FUN -> "Розваги"
    }

Тут немає else — і це добре. Чому? Бо якщо завтра ви додасте HEALTH, компілятор скаже: «Ага, функція categoryTitle() більше не покриває всі варіанти». І ви виправите це не «колись потім», а одразу.

Щоб краще відчути ідею, уявіть when за enum як сувору форму звіту: доки ви не заповнили всі рядки, звіт не приймають.

4. Чому else у when за enum часто шкідливий

Іноді рука тягнеться написати:

else -> "Невідомо"

Проблема в тому, що для enum «невідомо» найчастіше означає: «я додав новий варіант, а старий код мовчки проковтнув цю зміну й зробив щось дивне». Так, код компілюється. Але ціна — втрата автоматичної перевірки повноти.

Є ситуації, коли else усе-таки доречний. Наприклад, якщо ви свідомо не хочете розрізняти варіанти й задаєте типову поведінку. Але для логіки «таблиця відповідностей» майже завжди краще перелічити все явно.

Щоб порівняння було наочним, ось невелика таблиця:

Підхід Що виграємо Що втрачаємо
when за enum без else Компілятор перевіряє повноту, легше супроводжувати Потрібно справді обробити всі варіанти
when за enum з else Швидше «накинути», інколи зручно для прототипу Компілятор перестає допомагати під час розширення enum

5. Тип результату when-виразу

Коли when — це вираз, Kotlin має зрозуміти, який тип він повертає. Зазвичай усе просто: усі гілки повертають String — отже, when повертає String. Але інколи новачків дивує ситуація, коли гілки повертають різні типи, і компілятор починає «шукати спільний тип».

Для простоти запамʼятайте правило: усі гілки when-виразу мають повертати значення одного типу (або принаймні сумісного). Для enum це майже завжди природно: ви повертаєте або рядки, або числа, або якісь конкретні об’єкти.

Приклад: when повертає число

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

fun defaultLimit(category: ExpenseCategory): Int =
    when (category) {
        ExpenseCategory.FOOD -> 1500
        ExpenseCategory.TRANSPORT -> 800
        ExpenseCategory.HOME -> 2000
        ExpenseCategory.FUN -> 1000
    }

Приклад: спільний тип стає надто широким

enum class ExpenseCategory { FOOD, TRANSPORT }

fun weird(category: ExpenseCategory) =
    when (category) {
        ExpenseCategory.FOOD -> "їжа"
        ExpenseCategory.TRANSPORT -> 123
    }

Так можна написати, і Kotlin підбере спільний тип (найімовірніше Any). Але далі ви самі ускладните собі життя: замість зрозумілого String або Int ви отримаєте Any, і доведеться розбиратися, що саме там лежить. У реальному проєкті такий when майже завжди подає сигнал: «зупиніться, ви змішали дві різні задачі».

6. Nullable enum (E?): додаємо гілку null

У застосунках часто трапляється ситуація «значення ще не вибрано». Наприклад, категорію витрати не вказано, бо користувач увів лише суму. Тоді тип може бути ExpenseCategory?.

І тут важливе правило: якщо ви пишете when для nullable enum і хочете вичерпний when, потрібно обробити null окремою гілкою.

enum class ExpenseCategory { FOOD, TRANSPORT, HOME, FUN }

fun categoryTitleOrUnknown(category: ExpenseCategory?): String =
    when (category) {
        ExpenseCategory.FOOD -> "Їжа"
        ExpenseCategory.TRANSPORT -> "Транспорт"
        ExpenseCategory.HOME -> "Дім"
        ExpenseCategory.FUN -> "Розваги"
        null -> "Без категорії"
    }

Виходить трохи довше, зате чесно. І читається дуже прозоро: «якщо null, значить без категорії».

7. Приклад: when за enum у консольному застосунку

Зараз зберемо мініфрагмент логіки, який справді часто трапляється в CLI-застосунках: команди користувача. Раніше ми могли порівнювати рядки ("add", "list", "exit"), але в рядках легко помилитися, та й рефакторити їх незручно. enum робить список команд явним.

Оголосимо enum команд

enum class Command { ADD, LIST, HELP, EXIT }

when як вираз: робимо підказку

enum class Command { ADD, LIST, HELP, EXIT }

fun commandHelp(command: Command): String =
    when (command) {
        Command.ADD -> "add — додати витрату"
        Command.LIST -> "list — показати всі витрати"
        Command.HELP -> "help — показати довідку"
        Command.EXIT -> "exit — вийти з програми"
    }

Це класичний випадок «таблиці відповідностей»: enum → текст.

when як оператор: виконуємо дію

Нехай поки дії будуть простими (ми не ускладнюємо застосунок, а лише показуємо стиль).

enum class Command { ADD, LIST, HELP, EXIT }

fun runCommand(command: Command) {
    when (command) {
        Command.ADD -> println("Додавання...")   // Додавання...
        Command.LIST -> println("Список...")      // Список...
        Command.HELP -> println("Довідка...")     // Довідка...
        Command.EXIT -> println("Вихід...")       // Вихід...
    }
}

Зауважте: це також вичерпно, і else не потрібен.

Як when-вираз прибирає дублювання

Типова проблема новачків — повторювати одне й те саме в кожній гілці. Наприклад, ви хочете виводити результат однаковим способом: println(result). Тоді зручно спочатку отримати рядок із when, а потім один раз надрукувати його.

enum class Command { ADD, LIST, HELP, EXIT }

fun runCommandMessage(command: Command): String =
    when (command) {
        Command.ADD -> "Гаразд, додаємо витрату"
        Command.LIST -> "Показую список витрат"
        Command.HELP -> "Ось довідка щодо команд"
        Command.EXIT -> "Бувайте! (але код ще працює)"
    }

fun main() {
    val cmd = Command.HELP
    println(runCommandMessage(cmd)) // Ось довідка щодо команд
}

Тут ви чітко розділили відповідальність: when обирає текст, а println робить виведення. До того ж таке легше «прокрутити в голові» і простіше тестувати.

Невелика схема: when за enum як контрольоване розгалуження

Іноді корисно побачити, що when за enum — це буквально розвилка з фіксованими дорогами. Ось блок-схема того, що робить when-вираз (на рівні ідеї):

flowchart TD
    A[Отримали значення enum] --> B{"when(enum)"}
    B -->|Варіант 1| C[Результат/дія #1]
    B -->|Варіант 2| D[Результат/дія #2]
    B -->|Варіант 3| E[Результат/дія #3]
    B -->|Варіант N| F[Результат/дія #N]

Якщо enum має фіксований набір варіантів, компілятор може перевірити, що всі «дороги» існують. А якщо згодом ви додасте нову дорогу, компілятор вимагатиме додати й обробку — щоб користувач не «поїхав у кущі».

8. Типові помилки під час when з enum

Помилка №1: додавати else «про всяк випадок» і втрачати перевірку повноти.
Спочатку здається, що else — це страховка. Але для enum це часто перетворюється на пастку під час супроводу: ви розширили enum, а старий код продовжив компілюватися й поводитися не так, як задумано. Якщо ви справді хочете типову поведінку, краще явно назвати її: інколи це окремий enum-варіант (наприклад, UNKNOWN), а не мовчазний else.

Помилка №2: використовувати when як вираз, але повертати різні типи з гілок.
Kotlin підбере спільний тип, і ви раптово отримаєте Any, після чого почнуться дивні перевірки та приведення типів. У навчальних задачах це виглядає як «ну ж працює», а в реальному коді перетворюється на заплутаний контракт: незрозуміло, що саме повертає функція і як це використовувати.

Помилка №3: писати надто багато коду всередині гілок when.
when чудово читається, коли гілки короткі: повернути рядок, повернути число, викликати одну зрозумілу функцію. Коли всередині гілки зʼявляється пʼять println, цикл і ще один when, мозок починає просити відпустку. Хороший стиль: when обирає що зробити, а складна логіка живе в окремих функціях.

Помилка №4: забувати про null, коли enum nullable (E?).
Якщо ви перейшли на тип ExpenseCategory?, а в when не передбачили null, то або доведеться писати else, або ви отримаєте невичерпний when-вираз (і компілятор почне сваритися, якщо ви хочете повернути значення). У Kotlin null — це такий самий варіант, як і решта, просто він потребує окремої гілки.

Помилка №5: змішувати «вибір значення» і «виведення на екран» у кожній гілці.
Часто новачки роблять так: у кожній гілці when друкують щось, а потім ще раз друкують спільну частину. У результаті дублювання розростається. Зазвичай простіше зробити when виразом, отримати рядок, а потім один раз викликати println. Так код стає лінійним і передбачуваним.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ