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‑подход с такими helper‑функциями для ввода очень распространён: вы можете встретить подобные 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 }, чтобы быстро завершать сценарий при ошибке ввода и оставлять «успешный путь» ровным и понятным.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ