1. Диспетчеризація команд і when як «таблиця дій»
Коли ви пишете перші програми, часто все виглядає лінійно: запитали число, порахували, вивели. Та досить швидко зʼявляється бажання зробити програму «живою»: щоб вона приймала команди на кшталт help, add 10, show, exit і не завершувалася після однієї дії. Це і є найпростіший CLI (command-line interface). У такій програмі потрібно вміти вибирати дію за текстом, який увів користувач.
Уявіть, що ви робите міні застосунок «Гаманець v0» (без списків, без файлів, без баз даних — просто тренуємо логіку). Користувач вводить команди, а програма або змінює поточний стан (наприклад, баланс), або показує інформацію, або друкує довідку. Головне питання: як акуратно зіставити рядок команди з потрібною дією, щоб код не перетворився на «ліс ifʼів», у якому легко загубитися.
Тут when — ідеальний «диспетчер». Він читається як «таблиця команд». Kotlin позиціонує when як конструкцію для багатьох варіантів, схожу на switch, і підкреслює, що when можна використовувати і як оператор, і як вираз.
Мінімальний диспетчер команд на when
Коли у вас є нормалізована команда, when стає майже ідеальним диспетчером: кожна гілка — це команда, а else — зрозуміле повідомлення «я не знаю, що це». Такий код добре читається навіть людиною, яка бачить його вперше: зверху вниз — перелік підтримуваних команд.
Kotlin підкреслює, що when порівнює значення з гілками за порядком. Також він може бути або оператором (виконуємо дію), або виразом (повертаємо значення). Для командного диспетчера частіше підходить саме варіант оператора: «якщо команда така — зроби те».
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 тут використовується як оператор. У такій формі ви не зобовʼязані покривати всі варіанти, але для 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 як вираз має бути вичерпним (щоб завжди повертати значення), і тому у нас є гілка 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-оператор компілюється без else, у командному інтерфейсі «нічого не сталося» виглядає як баг. А якщо ви використовуєте when як вираз, то компілятор узагалі вимагатиме вичерпної обробки (зазвичай через else).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ