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 entry, 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 всё-таки уместен, например если вы сознательно не хотите различать варианты и делаете “default” поведение. Но именно для логики «таблица соответствий» почти всегда лучше перечислить всё явно.

Чтобы сравнение было наглядным, вот маленькая таблица:

Подход Что выигрываем Что теряем
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, а старый код продолжил компилироваться и вести себя не так, как задумано. Если вы действительно хотите “default”, лучше внятно назвать его: иногда это отдельный 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. Так код становится линейным и предсказуемым.

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