JavaRush /Курсы /Kotlin SELF /Диспетчеризация команд: нормализация ввода и when

Диспетчеризация команд: нормализация ввода и when

Kotlin SELF
13 уровень , 4 лекция
Открыта

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()
"help"
"help"
"help"
" Help "
"Help"
"help"
"EXIT"
"EXIT"
"exit"
"  add 10  "
"add 10"
"add 10"

Обратите внимание: "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).

1
Задача
Kotlin SELF, 13 уровень, 4 лекция
Недоступна
Командный пульт
Командный пульт
1
Задача
Kotlin SELF, 13 уровень, 4 лекция
Недоступна
Тишина в консоли
Тишина в консоли
1
Задача
Kotlin SELF, 13 уровень, 4 лекция
Недоступна
Команда и хвост
Команда и хвост
1
Задача
Kotlin SELF, 13 уровень, 4 лекция
Недоступна
Дежурный терминал
Дежурный терминал
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Серега PARADOX Уровень 21
2 апреля 2026
Прям столько внимания этому when, если бы вся лекция была в виде заданий по поэтапному написанию кошелька прям с нуля, было веселее и полезнее