1. Чому введення в main перетворюється на простирадло
Коли ви тільки починаєте, усе виглядає невинно: у main — кілька print(), кілька readln() — і ви вже отримуєте вік, зріст та улюблене число. Але минає 15 хвилин, і раптом ваш main схожий на бухгалтерський звіт на 40 рядків: усе — про введення, і майже нічого — про сенс програми. І це не тому, що ви поганий програміст: це цілком нормальний етап зростання.
Уявіть, що ми робимо просту консольну «анкету профілю». Наївний код зазвичай виглядає так:
fun main() {
print("Імʼя: ")
val name = readln()
print("Вік: ")
val ageStr = readln()
val age = ageStr.toIntOrNull()
if (age == null) {
println("Вік введено неправильно.")
return
}
println("Привіт, $name! Тобі $age років.")
// Привіт, Alex! Тобі 20 років.
}
Працює? Так. Зручно розширювати? Уже не дуже. Бо щойно ви додасте «зріст», «вагу», «поштовий індекс» і ще кілька полів, ви почнете копіювати той самий шаблон вручну. А копіпаст — як мокрий сніг: спершу «нічого страшного», а потім ви раптом ковзаєте й падаєте обличчям у баг.
У Kotlin (та й у програмуванні загалом) типова реакція на повтори — винести повторюваний фрагмент у функцію.
2. Prompt і утиліта readLine(prompt)
prompt — це параметр, а не рядок «поряд»
Слово prompt тут означає «підказка користувачеві перед введенням». Це буквально той текст, який ви друкуєте: "Вік: ", "Введіть ширину: " тощо. Здається дрібницею, але саме вона відповідає за дві важливі якості програми: зручність для людини й читабельність коду.
Зараз підказка майже завжди виглядає як пара рядків: print("..."), потім readln(). Це й породжує повтори. Якщо ми навчимося передавати prompt у функцію як параметр, то зможемо писати коротше й акуратніше: «викличте функцію — і вона сама покаже підказку та прочитає введення». Жодної магії — лише дисципліна: повторюваний код ховаємо всередину функції.
Важливо зафіксувати думку: утиліта введення має робити рівно одну справу — допомогти отримати значення. Вона не повинна «вирішувати долю програми», обирати гілку сценарію, друкувати довгі лекції про помилки тощо. Цим займеться main (або інший сценарний код), де контекст видно краще.
readLine(prompt) — читаємо рядок із підказкою
Почнемо з найпростішої (і дуже корисної) функції: друкуємо prompt, читаємо рядок і повертаємо його. Суперпросто — але вже прибирає копіпаст.
fun readLine(prompt: String): String {
print(prompt)
return readln()
}
fun main() {
val city = readLine("Місто: ")
println("Ви ввели місто: $city") // Ви ввели місто: Boston
}
Зверніть увагу на важливий момент: функція називається readLine, хоча в Kotlin є своя readLine() (яка повертала nullable). Ми тут використовуємо readln() — сучасний варіант, який повертає String і очікує, що введення справді буде (для навчальної консолі це нормально). У наших задачах сценарій «користувач узагалі нічого не ввів, і stdin завершився» трапляється рідко, тож поки не ускладнюємо.
І ще один момент: ми поки не робимо trim(), не нормалізуємо пробіли й не змінюємо регістр. Це свідоме рішення. Введення " Bob " зараз повернеться як є. Залишимо це як «маленький біль», який вилікуємо наступною лекцією, коли говоритимемо про чистий парсинг і нормалізацію.
3. Безпечний парсинг і функції ...OrNull
readIntOrNull(prompt): Int?
Тепер додамо найчастіший випадок: користувач має ввести ціле число. Ключова думка: не toInt(), тому що toInt() за неправильного формату викине виняток і зупинить програму. А ми зараз хочемо передбачувану поведінку: неправильне введення → null.
Зробімо функцію з чесною назвою: readIntOrNull. Суфікс OrNull — це як дорожній знак «обережно, тут може бути null». Код, що викликає, зобов’язаний це обробити.
fun readIntOrNull(prompt: String): Int? {
print(prompt)
val s = readln()
return s.toIntOrNull()
}
fun main() {
val age = readIntOrNull("Вік: ")
println("age = $age") // age = 20 (або age = null)
}
Чому це зручно:
Код введення став компактнішим, а головне — одноманітним і передбачуваним. Якщо у вас 10 полів, ви викличете readIntOrNull(...) 10 разів, а не писатимете 10 разів однакову зв’язку print/readln/toIntOrNull. І якщо ви вирішите поліпшити введення (наприклад, додати trim()), ви зробите це в одному місці.
Невеличка «пастка», яку корисно знати заздалегідь: якщо користувач введе " 10 " (із пробілами), toIntOrNull() поверне null. І це нормально для поточної лекції: ми поки будуємо базову цеглинку. У наступній лекції ми окремо поговоримо про нормалізацію введення — і саме там додамо trim() як системне рішення.
readDoubleOrNull(prompt): Double?
У консольних задачах Double трапляється не рідше за Int: ціна, зріст, вага, середня оцінка, координати, «скільки годин спав» тощо. І тут теж хочеться безпечного введення, щоб "3,14" або "три.чотири" не перетворювали програму на гарбуз.
Структура абсолютно така сама — і це добре: мозку легше запам’ятовувати, коли функції схожі.
fun readDoubleOrNull(prompt: String): Double? {
print(prompt)
val s = readln()
return s.toDoubleOrNull()
}
fun main() {
val height = readDoubleOrNull("Зріст (у метрах): ")
println("height = $height") // height = 1.82 (або height = null)
}
Практичне зауваження: багато користувачів вводять десяткові дроби через кому ("1,82"). toDoubleOrNull() такого не розуміє, бо очікує крапку. Ми поки не розв’язуємо цю проблему «в лоб», адже це вже про нормалізацію й правила — наступний щабель. Зараз наша мета проста: не падати й чесно повертати null.
Схема: прочитали → розпарсили → повернули T?
Щоб це закріпилося не як набір заклинань, а як зрозумілий процес, давайте намалюємо мініблок-схему. Вона однакова для Int, Double і будь-яких інших типів, які ви парситимете пізніше.
flowchart TD
A["Функція readXOrNull(prompt)"] --> B["print(prompt)"]
B --> C["raw = readln()"]
C --> D[attempt parse raw]
D --> E{вийшло?}
E -- так --> F[повернути значення T]
E -- ні --> G[повернути null]
Сенс у тому, що функція не повинна падати й не повинна ухвалювати рішення замість сценарію. Вона лише перетворює рядок на значення, якщо це можливо.
Kotlin-підхід із такими допоміжними функціями для введення дуже поширений: у практичних прикладах для задач ви можете зустріти подібні readInt()/readStr(), щоб скоротити код введення.
toInt() і toIntOrNull() та друзі
Початківці дуже часто плутають «безпечний парсинг» і «звичайний». Давайте розкладемо все по поличках, щоб потім не ловити сюрпризів.
| Що робимо | Метод | Що поверне за "123" | Що буде за "12a" |
|---|---|---|---|
| Парсим Int «напряму» | |
|
виняток (програма падає) |
| Парсим Int безпечно | |
|
|
| Парсим Double «напряму» | |
|
виняток |
| Парсим Double безпечно | |
|
|
Саме тому в користувацькому введенні ми майже завжди починаємо з ...OrNull: введення «брудне», і це нормально. Ми не зобов’язані вірити користувачеві на слово, особливо якщо він друкує на клавіатурі о 2-й годині ночі.
4. Як писати сценарій із T?
Elvis і ранній вихід
Поява Int? і Double? означає, що ваш сценарій має вміти жити з null. І тут часто з’являються дві крайнощі: або людина починає писати величезні вкладені if, або використовує !! (і програма знову падає — тільки вже з відчуттям провини).
Зробімо читабельний патерн: «не вийшло — повідомте й вийдіть». Для цього чудово підходить Elvis-оператор ?: разом із run { ... }, де ми можемо виконати кілька рядків і зробити return.
fun readIntOrNull(prompt: String): Int? {
print(prompt)
return readln().toIntOrNull()
}
fun main() {
val age = readIntOrNull("Вік: ") ?: run {
println("Вік має бути цілим числом.")
return
}
println("Гаразд, вік прийнято: $age") // Гаразд, вік прийнято: 20
}
Чому це гарно:
Код «успішного шляху» (happy path) не тоне в if (age != null) { ... }. Ми ніби кажемо: «отримайте вік, а якщо не вийшло — ось реакція, і виходимо». Це дуже людський стиль: спочатку страховка, потім основна логіка.
Мінізастосунок: анкета профілю
Щоб у нас була спільна історія коду, давайте оформимо маленький сценарій: питаємо імʼя, вік і зріст. Поки не робимо складних правил (діапазони, «не порожньо», повторне введення) — це буде пізніше. Сьогодні ми тренуємося в одному: стиснути шаблон введення до маленьких функцій.
Ось компактний варіант програми на поточному рівні знань:
fun readLine(prompt: String): String {
print(prompt)
return readln()
}
fun readIntOrNull(prompt: String): Int? {
print(prompt)
return readln().toIntOrNull()
}
fun readDoubleOrNull(prompt: String): Double? {
print(prompt)
return readln().toDoubleOrNull()
}
fun main() {
val name = readLine("Імʼя: ")
val age = readIntOrNull("Вік: ") ?: run {
println("Вік має бути цілим числом.")
return
}
val height = readDoubleOrNull("Зріст (м): ") ?: run {
println("Зріст має бути числом (наприклад, 1.82).")
return
}
println("Профіль: $name, $age років, зріст $height м")
// Профіль: Alex, 20 років, зріст 1.82 м
}
Зверніть увагу: ми не сперечаємося з користувачем і не намагаємося «вгадати», що він мав на увазі. Якщо введення некоректне — чесно кажемо про це й виходимо. Для початку це нормальна політика. Пізніше ми навчимося робити програми більш «терплячими» й просити ввести ще раз, але не змішуємо все в одну купу одразу.
5. Типові помилки
Помилка №1: використовувати toInt() / toDouble() для користувацького введення.
Це виглядає простіше, але на практиці перетворює будь-який неправильний символ на падіння програми. Сьогодні ми тренуємо протилежний стиль: «помилка введення — очікувана ситуація», тому toIntOrNull() і toDoubleOrNull() дають стабільніший контроль.
Помилка №2: назвати функцію readInt, хоча вона повертає Int?.
Імʼя — це обіцянка. Якщо ви назвали readInt, людина (зокрема й ви за тиждень) очікує Int, а не «може null, а може й не null». Суфікс OrNull — не бюрократія, а спосіб зробити код самодокументованим: побачили імʼя — одразу зрозуміли ризик.
Помилка №3: друкувати всередині утиліти довгі повідомлення про помилку.
Дуже хочеться, щоб readIntOrNull сама писала «Ви ввели нісенітницю!», «Спробуйте ще раз!», «Я у вас розчарований!». Але тоді утиліта перестає бути універсальною: в одному місці ви питаєте вік, в іншому — ширину, у третьому — кількість спроб, а повідомлення все одно однакове й недоречне. Краще так: утиліта повертає null, а сценарій вирішує, що саме сказати користувачеві.
Помилка №4: одразу «лікувати» всі проблеми введення всередині першої версії функції.
Наприклад, додати trim(), замінити кому на крапку, дозволити порожній рядок як 0 і ще п’ять «розумних» евристик. Це робить функцію непередбачуваною: сьогодні вона «здогадалася», завтра — «здогадалася неправильно». Ми рухаємося маленькими кроками: спочатку базова цеглинка, потім нормалізація (наступна лекція), потім валідація правил (ще пізніше).
Помилка №5: перетворювати main на ліс вкладених if.
Коли ви отримуєте Int?, рука тягнеться до if (x != null) { if (y != null) { if (z != null) { ... }}}. Це читається важко. Акуратніший стиль на нашому рівні — Elvis ?: + run { ...; return }, щоб швидко завершувати сценарій у разі помилки введення й залишати «успішний шлях» рівним і зрозумілим.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ