1. Зачем нужен when вместо длинных if/else if
Почти любая программа, которая общается с человеком (или с другой программой), рано или поздно начинает выбирать поведение по «коду»: по числу, по строке, по символу. Сначала всё выглядит нормально: два варианта — два if. Но как только вариантов становится 4–6, цепочка else if начинает вытягиваться вниз, как чек из супермаркета, где вы купили «что-то по акции» и внезапно осознали, что это был половник за 69 евро.
Проблема не в том, что if/else if плохой. Он отличный. Проблема в том, что длинная цепочка хуже читается: глазу сложнее увидеть «вот список всех вариантов», и сложнее понять, что будет, если значение не подходит ни под один вариант. Kotlin как раз даёт нам более «табличную» форму такого ветвления — when.
Начнём с очень типичной ситуации: есть значение, и мы хотим выполнить разные действия для разных значений.
fun main() {
val role = "editor"
if (role == "viewer") {
println("Доступ: только чтение")
} else if (role == "editor") {
println("Доступ: редактирование")
} else {
println("Неизвестная роль: $role")
}
}
Код рабочий. Но если ролей станет 6, это начнёт выглядеть менее дружелюбно. Тут и появляется when.
Идея when: «таблица соответствий» вместо «лестницы условий»
when (value) удобно читать так: «у меня есть значение value; если оно равно вот этому — делай то; если равно другому — делай другое; иначе — делай запасной вариант». По смыслу это очень похоже на switch из других языков, но в Kotlin синтаксис обычно более аккуратный и предсказуемый.
То есть вместо «лестницы» if/else if/else мы получаем конструкцию, которая визуально похожа на список вариантов. И это особенно приятно, когда вы пишете меню, обрабатываете коды ошибок, режимы работы, команды и прочие переключатели поведения.
Перепишем пример с ролью на when:
fun main() {
val role = "editor"
when (role) {
"viewer" -> println("Доступ: только чтение")
"editor" -> println("Доступ: редактирование")
else -> println("Неизвестная роль: $role")
}
}
Здесь глазом сразу видно «какие варианты поддерживаются» и «что будет иначе».
2. Синтаксис when (value) и правила чтения
Базовый синтаксис when (value) { ... }
Синтаксис when выглядит так: сначала пишем when (value), затем фигурные скобки, а внутри — ветки. Каждая ветка в базовом виде выглядит как «значение -> действие».
Важно привыкнуть к трём элементам:
- value — это то, что мы сравниваем (например, число меню или строка-режим).
- Слева от -> — «с чем сравниваем».
- Справа от -> — что выполнить, если совпало.
Мини-шаблон (почти как шпаргалка, но легальная):
fun main() {
val x = 2
when (x) {
1 -> println("Один") // Один
2 -> println("Два") // Два
else -> println("Другое")
}
}
Выглядит просто, но за этой простотой есть важная логика: проверки идут сверху вниз, и выполняется первая подходящая ветка.
else — ветка «на всякий случай», которая делает программу взрослее
Когда человек вводит данные, он иногда вводит не то. Иногда специально. Иногда случайно. Иногда кот прошёлся по клавиатуре — и это тоже пользовательский ввод, просто в «режиме кот». Поэтому ветка else в when — это страховка: «все остальные значения, которые я не перечислил».
При этом есть важный нюанс: если when используется как оператор (то есть просто выполняет действия), Kotlin не требует, чтобы вы обработали все варианты. Тогда, если значение не подошло ни под одну ветку, просто не выполнится ничего.
Однако в учебных и прикладных консольных программах почти всегда лучше явно писать else, чтобы пользователь получил понятный ответ.
Пример «культурной» обработки неизвестного значения:
fun main() {
val day = 9
when (day) {
1 -> println("Пн") // не выполнится
2 -> println("Вт") // не выполнится
3 -> println("Ср") // не выполнится
else -> println("Неизвестный день: $day") // Неизвестный день: 9
}
}
else почти всегда логичнее ставить в конце — тогда «таблица вариантов» читается сверху вниз, а «иначе» действительно означает «иначе».
Ветка как блок: когда в одной ветке нужно несколько действий
Часто кажется, что ветка when — это обязательно «одно действие в одну строку». На самом деле в ветке можно делать несколько действий: просто вместо одного выражения справа от -> ставится блок { ... }.
Это особенно полезно, когда вы хотите, например, вывести два сообщения, или сначала что-то посчитать, а потом напечатать результат. Пока мы используем when как оператор (то есть «делаем действия»), это выглядит очень естественно.
Пример с «многострочным ответом»:
fun main() {
val code = 400
when (code) {
200 -> println("OK") // не выполнится
400 -> {
println("Bad Request") // Bad Request
println("Проверьте запрос") // Проверьте запрос
}
else -> println("Другой код: $code")
}
}
Обратите внимание: фигурные скобки внутри ветки — это не «скобки when», а именно «скобки ветки», то есть мини-тело, где можно писать несколько строк.
Как выбирается ветка: порядок сверху вниз и «первая победившая»
Очень важно понимать механику выбора ветки, иначе можно случайно написать код, который выглядит логично, но работает «не так». when сравнивает значение с ветками по порядку, сверху вниз, и как только находит совпадение — выполняет эту ветку и выходит.
В этой лекции мы не будем делать ветки с диапазонами и условиями (это отдельная тема), но даже в простом сравнении «по равенству» полезно держать мысль: порядок веток — это часть логики.
Схема выполнения выглядит примерно так:
flowchart TD
A["Есть значение x"] --> B["Сравнить с первой веткой"]
B -->|совпало| C["Выполнить действия ветки 1 и выйти"]
B -->|не совпало| D["Сравнить со второй веткой"]
D -->|совпало| E["Выполнить действия ветки 2 и выйти"]
D -->|не совпало| F["..."]
F --> G["Если ничего не совпало → else (если есть)"]
Из этого следует простое правило стиля: else держим внизу, а варианты перечисляем сверху вниз так, как вы хотите, чтобы их читали люди.
3. Практика: меню «Кошелёк» и переход на when
Сейчас сделаем практический кусок, который хорошо показывает пользу when: обработка выбора пункта меню. Мы напишем маленький «кошелёк» — приложение, которое хранит текущий баланс (просто число) и предлагает пользователю выбрать действие. Мы не используем коллекции и сложные структуры данных: наша цель — научиться переключать поведение по значению, а не строить банк.
Печатаем меню и читаем выбор
fun main() {
println("=== Кошелёк ===")
println("1 — показать баланс")
println("0 — выход")
print("Ваш выбор: ")
val choice = readln().trim()
println("Вы выбрали: $choice") // например: Вы выбрали: 1
}
Пока выбор — строка. Это нормально: readln() всегда читает строку, а мы её подготавливаем через trim() (убираем лишние пробелы).
Превращаем ввод в число безопасно
Мы уже умеем так делать: toIntOrNull() возвращает Int?, то есть либо число, либо null, если пользователь ввёл не число.
fun main() {
print("Ваш выбор: ")
val choice = readln().trim().toIntOrNull()
if (choice == null) {
println("Нужно ввести число.") // Нужно ввести число.
return
}
println("Вы ввели номер: $choice")
}
Здесь мы используем «ранний выход» (return) — чтобы дальше работать только с корректными данными.
Та же логика через if/else if, чтобы было с чем сравнить
Пусть у нас есть баланс.
fun main() {
var balance = 100
print("Ваш выбор (1/0): ")
val choice = readln().trim().toIntOrNull() ?: return
if (choice == 1) {
println("Баланс: $balance") // Баланс: 100
} else if (choice == 0) {
println("Выход") // Выход
} else {
println("Неизвестная команда: $choice")
}
}
Работает, но уже видно, что это «лесенка».
Переписываем на when (choice)
Теперь делаем то же самое через when. По сути мы превращаем ветвление в «таблицу»: «1 → показать баланс», «0 → выход», «иначе → ошибка».
fun main() {
var balance = 100
print("Ваш выбор (1/0): ")
val choice = readln().trim().toIntOrNull() ?: return
when (choice) {
1 -> println("Баланс: $balance") // Баланс: 100
0 -> println("Выход") // Выход
else -> println("Неизвестная команда: $choice")
}
}
С точки зрения поведения программа не стала «умнее». Но с точки зрения чтения кода она стала спокойнее: меньше визуального шума, больше ощущения «списка вариантов».
Добавляем действий, чтобы почувствовать масштабируемость
Добавим пункты «пополнить» и «потратить». Сами операции сделаем максимально простыми: читаем число и меняем баланс. Тут уже when начинает выглядеть особенно уместно.
fun main() {
var balance = 100
println("1 — баланс, 2 — пополнить, 3 — потратить, 0 — выход")
print("Ваш выбор: ")
val choice = readln().trim().toIntOrNull() ?: return
when (choice) {
1 -> println("Баланс: $balance") // Баланс: 100
2 -> { print("Сумма: "); balance += readln().trim().toInt() }
3 -> { print("Сумма: "); balance -= readln().trim().toInt() }
0 -> println("Выход")
else -> println("Неизвестная команда: $choice")
}
}
Заметьте, что ветки 2 и 3 мы сделали блоками { ... }, потому что там два действия: спросить сумму и изменить баланс. Такой приём с блоком в ветке — нормальная часть when.
Да, тут можно улучшать безопасность (например, защищаться от нечисловой суммы), но это мы будем делать постепенно: сегодня наша цель — научиться выбирать поведение по значению красиво и читаемо.
Делаем меню «настоящим»: цикл + when
Меню обычно не одноразовое: пользователь выбирает действие несколько раз. У нас есть while, так что соберём простую версию «крутится, пока не выйдем». Внутри будет when, который маршрутизирует действия.
fun main() {
var balance = 100
var running = true
while (running) {
print("1-баланс 2+ 3- 0-выход: ")
val choice = readln().trim().toIntOrNull() ?: continue
when (choice) {
1 -> println("Баланс: $balance")
2 -> { print("Сумма: "); balance += readln().trim().toInt() }
3 -> { print("Сумма: "); balance -= readln().trim().toInt() }
0 -> { println("Пока!"); running = false } // Пока!
else -> println("Неизвестно: $choice")
}
}
}
Обратите внимание на аккуратную связку: если choice не распарсился, мы делаем continue и просто повторяем ввод. Это не тема when, но это типичный «скелет» консольных программ: цикл + чтение + ветвление.
4. Типичные ошибки при работе с when (value)
В начале when обычно кажется «слишком простым, чтобы ошибаться», но ошибки всё равно возникают — в основном из-за читаемости и привычек после if/else. Хорошая новость: эти ошибки редко сложные, и почти всегда их видно глазами, если научиться читать when как таблицу вариантов, а не как «ещё один if».
Ошибка №1: забывают про else и получают «молчаливое поведение».
Технически when как оператор может не быть исчерпывающим: если ни одна ветка не совпадёт, не выполнится ничего. Но в учебных задачах и CLI‑программах это обычно плохой UX: пользователь вводит «9», а программа будто зависла (хотя она просто ничего не сделала). Чаще всего лучше явно писать else и печатать понятное сообщение.
Ошибка №2: ставят else не последним и прячут смысл веток ниже.
Логика when — сверху вниз: первая подходящая ветка «побеждает». Если вы поставите else раньше, всё, что ниже, станет недостижимым или бессмысленным. Даже когда компилятор это не ругает, читатель кода начинает нервничать: «зачем там ветки, если есть else выше?». Держите else в конце как правило стиля.
Ошибка №3: раздувают ветки до 20–30 строк и превращают when в «мини-программу».
when хорош как диспетчер: выбрать вариант и запустить нужное действие. Когда внутри ветки появляется половина приложения, «таблица вариантов» перестаёт быть таблицей и становится длинным полотном. В таких случаях помогает дисциплина: ветка короткая, а подробности уезжают в отдельную функцию (вы уже умеете их писать).
Ошибка №4: пытаются заменить if там, где всего два варианта.
По конвенциям Kotlin обычно предпочитают if для бинарного выбора, а when — когда вариантов три и больше. Это не «закон», но хороший ориентир: если у вас «да/нет» — if часто читается проще, чем when ради одной проверки и else.
Ошибка №5: сравнивают «грязный ввод» как есть и удивляются, что ветки не срабатывают.
Сегодня мы в основном сравнивали числа, но даже там есть «грязь» (пробелы, пустая строка, нечисловые символы). Если значение приходит строкой, то без нормализации (trim(), иногда lowercase()) можно получить ситуацию «пользователь ввёл правильно, но программа считает иначе». Привычка «сначала подготовь ввод, потом ветвись» экономит часы отладки и немного сохраняет веру в человечество.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ