JavaRush /Курси /Kotlin SELF /Утиліти введення: prompt → парсинг → T?

Утиліти введення: prompt → парсинг → T?

Kotlin SELF
Рівень 14 , Лекція 1
Відкрита

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 «напряму»
toInt()
123
виняток (програма падає)
Парсим Int безпечно
toIntOrNull()
123
null
Парсим Double «напряму»
toDouble()
123.0
виняток
Парсим Double безпечно
toDoubleOrNull()
123.0
null

Саме тому в користувацькому введенні ми майже завжди починаємо з ...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 }, щоб швидко завершувати сценарій у разі помилки введення й залишати «успішний шлях» рівним і зрозумілим.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ