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‑подход с такими helper‑функциями для ввода очень распространён: вы можете встретить подобные 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 }, чтобы быстро завершать сценарий при ошибке ввода и оставлять «успешный путь» ровным и понятным.

1
Задача
Kotlin SELF, 14 уровень, 1 лекция
Недоступна
Профиль путешественника
Профиль путешественника
1
Задача
Kotlin SELF, 14 уровень, 1 лекция
Недоступна
Пропускной возраст
Пропускной возраст
1
Задача
Kotlin SELF, 14 уровень, 1 лекция
Недоступна
Ценник со скидкой
Ценник со скидкой
1
Задача
Kotlin SELF, 14 уровень, 1 лекция
Недоступна
Фитнес проверка
Фитнес проверка
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ