1. Диспетчеризация команд и when как «таблица действий»
Когда вы пишете первые программы, часто всё выглядит линейно: спросили число, посчитали, вывели. Но довольно быстро появляется желание сделать программу «живой»: чтобы она принимала команды вроде help, add 10, show, exit и не завершалась после одного действия. Это и есть простейший CLI (command-line interface), и в нём нужно уметь выбирать, что делать, по тексту пользователя.
Представьте, что вы делаете мини-приложение «Кошелёк v0» (без списков, без файлов, без баз данных — просто тренируем логику). Пользователь вводит команды, а программа либо меняет текущее состояние (например, баланс), либо показывает информацию, либо печатает справку. Главный вопрос: как аккуратно сопоставить строку-команду с нужным действием, чтобы код не превратился в «лес if-ов», в котором легко потеряться.
И здесь when — идеальный диспетчер: он читается как «таблица команд». Kotlin прямо позиционирует when как конструкцию для множественных вариантов, похожую на switch, и подчёркивает, что when можно использовать и как оператор, и как выражение.
Минимальный диспетчер команд на when
Когда у вас есть нормализованная команда, when становится почти идеальным диспетчером: каждая ветка — это команда, а else — понятное сообщение «я не знаю, что это». Такой код хорошо читается даже человеком, который впервые его видит: сверху вниз — список поддерживаемых команд.
Kotlin подчёркивает, что when сравнивает значение с ветками по порядку и может использоваться как statement (выполняем действие) или expression (возвращаем значение). Для командного диспетчера чаще подходит именно statement-стиль: «если команда такая — сделай то».
fun main() {
val command = readln().trim().lowercase()
when (command) {
"help" -> println("Команды: help, exit") // Команды: help, exit
"exit" -> println("Завершаю работу...") // Завершаю работу...
else -> println("Неизвестная команда: $command")
}
}
Здесь важно, что else стоит в конце. Это не просто «традиция»: если вы поставите else раньше, нижние ветки станут недостижимыми, и компилятор (и будущий вы) будут недовольны.
2. Подготовка ввода: нормализация, пустой ввод, команда и аргумент
Нормализация ввода: trim() и lowercase()
Если сравнивать ввод пользователя с командами «как есть», программа будет обижаться на пробелы и регистр. Команда Help, HELP, help и help с точки зрения человека — одно и то же. С точки зрения «сырой строки» — четыре разных значения. Поэтому перед тем как что-то анализировать, ввод принято нормализовать: убрать пробелы по краям и привести к одному регистру.
Самый базовый и почти всегда достаточный набор действий: trim() (убираем пробелы по краям) и lowercase() (делаем всё маленькими буквами). Важно: trim() не удаляет пробелы внутри строки, и это хорошо — иначе команда add 10 превратилась бы в add10, и аргумент потерялся бы как носок после стирки.
Посмотрим на мини-пример: читаем строку и делаем из неё «стабильную версию», пригодную для сравнения.
fun main() {
val raw = readln()
val normalized = raw.trim().lowercase()
println("raw='$raw'") // raw=' HeLp '
println("normalized='$normalized'") // normalized='help'
}
Теперь программа сравнивает только normalized. Это не «магия», а дисциплина: мы один раз приводим данные к стандарту, и дальше живём спокойно.
Чтобы почувствовать смысл нормализации, полезно держать в голове такую таблицу:
| Ввод пользователя | После trim() | После trim().lowercase() |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Обратите внимание: "add 10" после lowercase() не ломается — пробел внутри остаётся, и мы сможем отделить аргумент позже.
Пустая строка — это отдельный случай
Когда пользователь нажимает Enter, иногда он вводит пустую строку. И это не редкость: люди ошибаются, промахиваются, тестируют «а что будет если…», или просто кот прошёлся по клавиатуре (кот — главный QA-инженер в мире, просто он не подписывает баг-репорты).
Пустую строку нужно обработать явно, потому что иначе в when вы получите «неизвестную команду: ""», и пользователю будет непонятно, что произошло. Технически программа права, но психологически она звучит как: «неизвестный запрос: (тишина)».
Самый простой паттерн: после trim() проверить isEmpty() и сделать понятную реакцию — например, продолжить цикл или вывести подсказку.
fun main() {
val input = readln().trim()
if (input.isEmpty()) {
println("Введите команду. Для справки: help") // Введите команду. Для справки: help
return
}
println("Команда принята: '$input'") // например: Команда принята: 'help'
}
Мы здесь делаем return, потому что пример короткий. В реальной «живой» программе (где есть цикл) вы чаще сделаете continue, чтобы просто перейти к следующему вводу.
Команда + аргумент без split(): indexOf(' ') и substring(...)
Команды часто приходят с аргументом: add 10, set 100, echo привет. Можно было бы использовать split(" "), но мы сознательно не идём туда сегодня: во‑первых, это отдельная тема «разбор строк» во всей красе, во‑вторых, split легко порождает лишние детали (пустые части, много пробелов и т.д.), а нам сейчас нужен максимально контролируемый, понятный вручную разбор.
Идея простая: ищем первый пробел. Всё слева — команда. Всё справа — аргумент (возможно пустой). Если пробела нет — аргумента нет.
Схематично это можно представить так:
flowchart TD
A["raw input: ' add 10 '"] --> B["trim(): 'add 10'"]
B --> C["lowercase(): 'add 10'"]
C --> D["indexOf(' ') -> 3"]
D --> E["command = substring(0, 3) -> 'add'"]
D --> F["arg = substring(4).trim() -> '10'"]
E --> G["when(command) {...}"]
Теперь — практический код. Мы сделаем выделение command и arg максимально «в лоб», чтобы было понятно, что именно происходит.
fun main() {
val raw = readln().trim()
val spaceIndex = raw.indexOf(' ')
val command = if (spaceIndex == -1) raw.lowercase() else raw.substring(0, spaceIndex).lowercase()
val arg = if (spaceIndex == -1) "" else raw.substring(spaceIndex + 1).trim()
println("command='$command', arg='$arg'") // например: command='add', arg='10'
}
Пара нюансов, которые обычно спасают от странных багов:
- Во-первых, аргумент мы дополнительно trim()-им, чтобы add 10 всё равно дал аргумент 10, а не " 10".
- Во-вторых, команду мы нормализуем через lowercase() (и обычно после trim()), потому что именно её будем сравнивать.
3. Проект: «Кошелёк v0»
Сейчас мы соберём всё в маленькую консольную программу, которая живёт в цикле, принимает команды и меняет одно число — баланс. Это не «финальное приложение мечты», а учебный тренажёр: он учит вас писать предсказуемую обработку команд и не путаться в строках. В следующих днях курса мы будем усиливать подход (и уже тогда появятся более мощные инструменты), но сегодня наша цель — чистая диспетчеризация.
Каркас цикла: читаем команды, пока не попросили выйти
Начнём с самого простого: бесконечный цикл, чтение строки, команда exit — выход.
fun main() {
while (true) {
val cmd = readln().trim().lowercase()
if (cmd == "exit") break
println("Вы ввели: $cmd") // например: Вы ввели: help
}
println("Пока!") // Пока!
}
Это ещё не when, но это минимальный «пульс» командной программы.
Добавляем таблицу команд через when
Теперь заменим if (cmd == "exit") на when. Важно: мы хотим, чтобы exit действительно завершал цикл, поэтому внутри ветки используем break.
fun main() {
while (true) {
val cmd = readln().trim().lowercase()
when (cmd) {
"help" -> println("Команды: help, show, exit") // Команды: help, show, exit
"show" -> println("Пока показывать нечего") // Пока показывать нечего
"exit" -> break
else -> println("Неизвестная команда: $cmd")
}
}
}
Обратите внимание: when здесь используется как statement (оператор). В statement-форме вы не обязаны покрывать все варианты, но практически для CLI else всё равно нужен — иначе «тишина» будет выглядеть как поломка.
Переходим к «команда + аргумент» и вводим состояние balance
Теперь добавим переменную balance и команды, которые меняют её. Мы снова используем паттерн «прочитал → подготовил → распарсил», чтобы не пытаться toInt() на случайном тексте.
fun main() {
var balance = 0
val raw = readln().trim()
val spaceIndex = raw.indexOf(' ')
val command = if (spaceIndex == -1) raw.lowercase() else raw.substring(0, spaceIndex).lowercase()
val arg = if (spaceIndex == -1) "" else raw.substring(spaceIndex + 1).trim()
println("Баланс=$balance, command=$command, arg=$arg") // Баланс=0, command=add, arg=10
}
Пока мы только «разобрали строку». Теперь — самое интересное: команда определяет действие, аргумент — данные для действия.
Команды, которым нужно число: toIntOrNull() и понятные сообщения
Числовой аргумент нужно парсить безопасно. Мы уже знаем toIntOrNull() и то, что он возвращает Int?, то есть либо число, либо null. Дальше у нас есть два хороших пути: либо if (value == null), либо when (value) с веткой null. Это удобный способ не плодить вложенные проверки.
Сделаем небольшой фрагмент логики: команда add, аргумент — число.
fun main() {
val arg = "10"
val delta: Int? = arg.toIntOrNull()
when (delta) {
null -> println("Нужно число, а не '$arg'") // если arg="abc"
else -> println("Ок, прибавим $delta") // Ок, прибавим 10
}
}
Теперь переносим этот подход в обработку команд.
Почти финальная версия «Кошелька v0»
Ниже — компактная версия, которую можно собрать у себя целиком. Она всё ещё короткая, но уже реально полезная как шаблон. Сообщения сделаны «человеческими»: CLI — это не экзамен по телепатии.
fun main() {
var balance = 0
while (true) {
val raw = readln().trim()
if (raw.isEmpty()) continue
val spaceIndex = raw.indexOf(' ')
val command = if (spaceIndex == -1) raw.lowercase() else raw.substring(0, spaceIndex).lowercase()
val arg = if (spaceIndex == -1) "" else raw.substring(spaceIndex + 1).trim()
when (command) {
"help" -> println("Команды: help, show, set N, add N, sub N, exit")
"show" -> println("Баланс: $balance") // Баланс: 0
"exit" -> break
"set", "add", "sub" -> {
val value = arg.toIntOrNull()
if (value == null) {
println("Нужно число после '$command'") // Нужно число после 'add'
} else {
balance = when (command) {
"set" -> value
"add" -> balance + value
else -> balance - value // sub
}
println("Ок. Баланс: $balance")
}
}
else -> println("Неизвестная команда: $command")
}
}
println("Выход. Финальный баланс: $balance")
}
Внутри ветки "set", "add", "sub" мы использовали ещё один when как выражение: возвращаем новое значение баланса. when-expression должен быть исчерпывающим (чтобы всегда вернуть значение), и поэтому у нас есть ветка else (она здесь логически означает sub).
Да, это «двойной when», но он читается нормально: внешний when выбирает команду, внутренний — формулу обновления баланса. И главное: мы не смешали в одну кашу распознавание команды и математику — сначала выделили command/arg, потом обработали.
4. when (value) и when { ... } в командном коде
На этом этапе многие начинают путаться: «А можно я всегда буду делать when { ... }, потому что он похож на if?» Можно, но не всегда нужно. Главное правило — выбирать форму, которая делает код очевиднее.
Если вы сравниваете одно значение с набором вариантов (например, command со строками "help", "exit"), то when (command) читается как справочник команд. Если же у вас логика построена на условиях (например, «команда пустая», «слишком короткая», «начинается с #»), то удобнее when { ... }.
Мини-пример: «качество ввода» как набор условий.
fun main() {
val input = readln().trim()
val status = when {
input.isEmpty() -> "пусто"
input.length < 3 -> "слишком коротко"
else -> "похоже на команду"
}
println(status) // например: похоже на команду
}
А вот when (command) — для диспетчеризации:
fun main() {
val command = readln().trim().lowercase()
when (command) {
"help" -> println("Помощь") // Помощь
"exit" -> println("Выход") // Выход
else -> println("Не понимаю: $command")
}
}
В командном интерфейсе чаще всего вы комбинируете оба подхода: сначала when { ... } для «проверить и отфильтровать мусор», потом when (command) для «таблицы команд».
5. Типичные ошибки
Ошибка №1: сравнивать команды без нормализации.
Если вы делаете when (readln()) { "help" -> ... }, то команда " help" (с пробелом) или "HELP" (в верхнем регистре) внезапно становится «неизвестной». Формально программа права, но пользователь ощущает себя неправым — и перестаёт доверять программе. Правильнее сначала сделать trim().lowercase(), и только потом сравнивать.
Ошибка №2: пытаться парсить аргумент до того, как вы убедились, что он вообще нужен.
Иногда начинающий код делает val n = arg.toInt() сразу после чтения строки, а потом уже думает, какая команда была введена. В результате команда help тоже пытается распарсить «аргумент», которого нет, и программа падает. Сначала определяем команду, потом понимаем, нужен ли аргумент, и только потом парсим.
Ошибка №3: не обрабатывать пустую строку.
Если не выделить пустой ввод в отдельный случай, вы будете получать либо «неизвестную команду» без объяснений, либо странные ошибки, если дальше в коде предполагается, что команда не пустая. Пара строк с if (raw.isEmpty()) continue резко повышает дружелюбность и устойчивость программы.
Ошибка №4: делать ветки when слишком большими и «многоэтажными».
when хорош как диспетчер, но он быстро превращается в «мини-программу внутри программы», если в каждой ветке по 20 строк логики. Сегодня мы сознательно держали ветки короткими: распознавание команды — отдельно, вычисление баланса — отдельно. Kotlin подчёркивает, что ветки могут быть блоками, и это удобно, но это не приглашение писать роман внутри одной ветки.
Ошибка №5: забыть про else там, где он нужен по смыслу.
Даже если when-statement компилируется без else, в командном интерфейсе «ничего не произошло» выглядит как баг. А если вы используете when как выражение, то компилятор вообще потребует исчерпывающую обработку (обычно через else).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ