Перейти к основному содержимому
Мобилка09ГЛАВА 09Взаимодействие с консольюСколько вам лет?абвНужно целое числоСколько вам лет?17readlnOrNull() → String?приходит строка

Взаимодействие с консолью: ввод, разбор, вывод

Прочитано0%
Квизы0/5
Уровень 1 · Новичок

Зачем это нужно​

До сих пор все программы в главах были немыми: значения зашиты в код, результат виден в выводе. Первый шаг к настоящей программе — дать ей поговорить с человеком.

Консоль для этого — самый дешёвый инструмент. Ни экранов, ни разметки, ни эмулятора: две строчки, и программа спрашивает и отвечает. Именно поэтому с консоли начинают, и именно поэтому она стоит в программе подготовки отдельным пунктом.

Но есть и вторая причина, менее очевидная. Работа с вводом — это первое место, где приходится всерьёз иметь дело с чужими данными: человек введёт не то, что вы ждали. Пустую строку. Пробелы. «сорок два» вместо «42». Привычка проверять ввод, приобретённая на консоли, потом переносится на формы в приложении — где цена ошибки уже в баллах.

Две функции, и одна из них устарела​

Прочитать строку с клавиатуры можно так:

val line = readLine() // String? — вернёт null, когда ввод кончился
val line = readlnOrNull() // String? — то же самое, современное имя
val line = readln() // String — бросит исключение, когда ввод кончился

Во всех старых учебниках стоит readLine(). И это имеет значение:

Важно

Начиная с Kotlin 2.1.0 компилятор помечает readLine() устаревшей:

'fun readLine(): String?' is deprecated. Use `readln()` instead of `readLine()!!`,
and `readlnOrNull()` instead of `readLine()`.

Проверено по версиям: на 2.0.21 предупреждения ещё нет, начиная с 2.1.0 — есть во всех. То есть в проекте на старом компиляторе readLine() промолчит, а в свежем подчеркнётся. Пишите сразу readlnOrNull(): это то же самое, только без будущих претензий.

Разница между readln() и readlnOrNull() — в том, что происходит, когда читать больше нечего:

  • readlnOrNull() вернёт null;
  • readln() бросит исключение.

Вот настоящий текст этого исключения:

kotlin.io.ReadAfterEOFException: EOF has already been reached
Совет

За клавиатурой человек ввод не «заканчивает» — поэтому в учебной программе readln() выглядит безопасным. Но как только программу запустят, подав ей файл (программа < data.txt) или нажав Ctrl+D, разница станет заметной. Привыкайте к readlnOrNull() и явной обработке конца ввода.

Что на самом деле приходит​

Главная ловушка ввода — думать, что приходит то, что человек «имел в виду». Приходит строка, и она бывает неожиданной.

ЧТО ПРИХОДИТ С КЛАВИАТУРЫнажали Enter, ничего не введя«»пустая строка, НЕ null · длина 0ввели « Олег »« Олег »пробелы сохраняются · длина 10ввели 42«42»это СТРОКА, а не числоввод кончился (файл, Ctrl+D)nullтолько здесь бывает null«пусто» и «ввода больше нет» — разные вещи: первое это строка длиной ноль
Четыре разных случая и то, что возвращает чтение

Живой прогон — программа запущена с настоящим вводом:

── 1. пустая строка: просто Enter ──
readlnOrNull() → «», длина 0
isEmpty=true isBlank=true
── 2. пробелы по краям ──
как есть: « Олег » (длина 10)
trim(): «Олег» (длина 4)
── 3. число строкой ──
«42».toIntOrNull() = 42
── 7. ввод кончился ──
readlnOrNull() = null
readln() бросил kotlin.io.ReadAfterEOFException: EOF has already been reached

Три вывода отсюда:

  1. Нажатый Enter даёт пустую строку, а не null. null бывает только когда ввод закончился. Путать эти случаи нельзя: if (line == null) не поймает пользователя, который просто нажал Enter.
  2. Пробелы приходят как есть. Строка « Олег » имеет длину 10, и сравнение line == "Олег" будет ложным.
  3. С клавиатуры всегда приходит строка. Даже когда ввели 42.
Интересный факт

isEmpty() и isBlank() — разные проверки. Первая спрашивает «длина ноль?», вторая — «нет ничего, кроме пробелов?». Для строки из трёх пробелов isEmpty() даст false, а isBlank() — true. Для ввода почти всегда нужна вторая.

Отвечено вопросов: 0 из 3
Пользователь нажал Enter, ничего не введя. Что вернёт readlnOrNull()?
Ввели « Олег ». Сработает ли сравнение line == "Олег"?
Чем readln() отличается от readlnOrNull()?
Отвечено верно: 0 из 3

Превратить строку в число​

Строку «42» нельзя складывать. Нужен разбор, и здесь у Kotlin две пары функций:

"42".toInt() // 42, но бросит исключение на «абв»
"42".toIntOrNull() // 42, а на «абв» вернёт null
"3.5".toDouble() // 3.5, но бросит исключение на «абв»
"3.5".toDoubleOrNull() // 3.5, а на «абв» вернёт null

Вот настоящий текст исключения от toInt():

NumberFormatException: For input string: "сорок два"
Важно

Для данных, введённых человеком, используйте только варианты с OrNull. Не потому, что исключения плохи, а потому, что неверный ввод — это не исключительная ситуация, а самая обычная. Программа, падающая от опечатки, — сломанная программа.

А теперь неочевидное. Посмотрите, что эти функции делают с разными строками:

fun main() { val cases = listOf("42", "3.5", "3,5", "-5", "007", " 42 ", "99999999999", "") println("строка toIntOrNull toDoubleOrNull") for (s in cases) { println("«%-13s» %-13s %s".format(s, s.toIntOrNull().toString(), s.toDoubleOrNull().toString())) } }

Настоящий вывод:

строка toIntOrNull toDoubleOrNull
«42 » 42 42.0
«3.5 » null 3.5
«3,5 » null null
«-5 » -5 -5.0
«007 » 7 7.0
« 42 » null 42.0
«99999999999 » null 9.9999999999E10
« » null null
ОДНА И ТА ЖЕ СТРОКА, ДВА РАЗБОРАtoIntOrNull()toDoubleOrNull()«42»4242.0«3.5»null3.5точка — годится«3,5»nullnullзапятая — нет« 42 »null42.0Int пробелов не прощает«99999999999»null9.99…E10не влезает в Inttrim() перед разбором — не аккуратность, а условие работы: без него « 42 » не число
Одна строка, два разбора — и разное поведение на краях

Разберём строки, которые удивляют:

  • «3,5» — null у обеих. Запятая как разделитель дробной части не понимается. Только точка. А человек в России напишет именно запятую.
  • « 42 » — toIntOrNull даёт null, а toDoubleOrNull даёт 42.0. Асимметрия неожиданная и обидная: разбор целого пробелов не прощает, разбор дробного — прощает.
  • «007» — 7. Ведущие нули срезаются молча.
  • «99999999999» — null у toIntOrNull. Не потому, что это не число, а потому, что оно не влезает в Int: предел — 2 147 483 647.
Совет

Отсюда простое правило: trim() перед разбором ставится всегда. Это не аккуратность, а условие работы — без него строка с лишним пробелом не превратится в целое число, и программа скажет «это не число» про совершенно правильные 42.

Спрашивать, пока не ответят правильно​

Собираем всё вместе. Задача: получить возраст — целое число от 1 до 120, и не сдаваться, пока не получим.

// Плейграунд не даёт клавиатуру, поэтому ввод подменён заранее заготовленным. // В настоящей программе на месте input() стоит readlnOrNull(). private val scripted = listOf("абв", "200", "0", "17").iterator() fun input(): String? = if (scripted.hasNext()) scripted.next().also { println(it) } else null /** Спрашивает, пока не введут целое число в заданных границах. */ fun askInt(prompt: String, range: IntRange): Int { while (true) { print("$prompt ") val line = input() ?: error("ввод кончился") val n = line.trim().toIntOrNull() when { n == null -> println(" Нужно целое число, а получено «${line.trim()}»") n !in range -> println(" Число должно быть от ${range.first} до ${range.last}, а получено $n") else -> return n } } } fun main() { val age = askInt("Сколько вам лет?", 1..120) println("Принято: $age") }

Настоящий вывод:

Сколько вам лет? абв
Нужно целое число, а получено «абв»
Сколько вам лет? 200
Число должно быть от 1 до 120, а получено 200
Сколько вам лет? 0
Число должно быть от 1 до 120, а получено 0
Сколько вам лет? 17
Принято: 17
Нажми на часть выражения

Обратите внимание на сообщения об ошибках. Каждое говорит что не так и что получено: «Нужно целое число, а получено «абв»». Не «Ошибка ввода», не «Неверное значение». Эта привычка потом пригодится на формах: подпись под полем должна объяснять, а не констатировать.

Отвечено вопросов: 0 из 3
Почему trim() ставится ДО toIntOrNull(), а не после?
Строка «3,5». Что вернёт toDoubleOrNull()?
Почему для ввода человека берут toIntOrNull(), а не toInt()?
Отвечено верно: 0 из 3

Несколько значений в одной строке​

Часто удобнее ввести всё сразу: Олег 17 Самара. Разбирается это через split:

val parts = line.split(" ")

Живой прогон:

split даёт 3 части: [Олег, 17, Самара]
имя=Олег возраст=17 город=Самара

Соблазнительно разложить сразу по переменным:

val (name, age, city) = parts
Важно

Так делать опасно. Если частей окажется меньше, чем переменных, программа падает:

IndexOutOfBoundsException: Index: 1, Size: 1

Это настоящий вывод: строку «Олег» разложили на две переменные. Проверять размер надо до разбора:

val parts = line.trim().split(" ").filter { it.isNotBlank() }
if (parts.size != 3) {
println("Нужно три значения через пробел, а получено ${parts.size}")
return
}

filter { it.isNotBlank() } здесь не украшение: split(" ") по строке с двойным пробелом даст пустую часть посередине, и размер не сойдётся.

Вывод: print, println и формат​

Две функции вывода отличаются одним:

  • println("текст") — печатает и переводит курсор на новую строку;
  • print("текст") — печатает и оставляет курсор на месте.

Именно поэтому вопрос печатают через print: ответ вводится сразу после него, на той же строке.

Для чисел часто нужен формат — скажем, две цифры после точки:

"%.2f".format(1234.5)

И здесь ждёт ловушка, которую видно только на настоящем запуске.

Важно

Один и тот же код даёт разный результат в зависимости от языка системы. Проверено на двух машинах:

локаль ru_RU: "%.2f".format(1234.5) = 1234,50 ← запятая
локаль en_US: "%.2f".format(1234.5) = 1234.50 ← точка

А теперь соедините это с предыдущей секцией: строку «1234,50» функция toDoubleOrNull() прочитать не может. То есть программа печатает то, что сама же не в состоянии разобрать обратно.

Если формат должен быть предсказуемым, локаль задаётся явно:

"%.2f".format(Locale.US, 1234.5) // всегда 1234.50

Отдельно стоит запомнить: в главах этой платформы примеры выполняются в плейграунде, где локаль en_US, а телефон студента, скорее всего, живёт в ru_RU. Проверить форматирование в плейграунде и решить, что на устройстве будет так же, — ошибка.

Отвечено вопросов: 0 из 3
Почему вопрос печатают через print, а не println?
"%.2f".format(1234.5) на системе с русским языком даёт…
Проверили форматирование в плейграунде — на телефоне будет так же?
Отвечено верно: 0 из 3

Консоль — это слой, а не программа​

Самая важная мысль главы, и она про то, что будет дальше.

Программу с консолью легко написать так, что чтение, подсчёт и печать перемешаны. Работать будет. Но перенести такой код в приложение невозможно: на экране Android печатать нечем — там нет консоли.

КТО ЧИТАЕТ, КТО СЧИТАЕТ, КТО ПЕЧАТАЕТmain — разговор с человекомreadlnOrNull()trim() · toIntOrNull()println(…)вся печать и всё чтение — только тутчислоCart — только логикаadd(title, count): Resulttotal(): Intни одного printlnпро клавиатуру не знает вовсетакой класс переносится в приложение без единой правки: экран заменит собой mainкласс, который печатает сам, придётся переписывать — печатать на экране Android нечем
Логика ничего не печатает; печатает и спрашивает только внешний слой

Правильное разделение: класс считает и возвращает результат, а печатает и спрашивает только main.

// Логика ничего не печатает и ничего не читает — она только считает. class Cart { private val lines = mutableMapOf<String, Int>() fun add(title: String, count: Int): Result<Int> { if (title.isBlank()) return Result.failure(IllegalArgumentException("пустое название")) if (count <= 0) return Result.failure(IllegalArgumentException("количество должно быть больше нуля")) val now = lines.getOrDefault(title, 0) + count lines[title] = now return Result.success(now) } fun total(): Int = lines.values.sum() fun items(): List<Pair<String, Int>> = lines.toList() } // Консоль — отдельный слой: он читает, спрашивает и печатает. private val scripted = listOf("Nike 2", "Adidas 0", " 3", "Puma 1", "").iterator() fun input(): String? = if (scripted.hasNext()) scripted.next().also { println(it) } else null fun main() { val cart = Cart() println("Вводите «название количество», пустая строка — конец.") while (true) { print("> ") val line = input()?.trim() ?: break if (line.isEmpty()) break val parts = line.split(" ").filter { it.isNotBlank() } if (parts.size != 2) { println(" Нужно два значения через пробел, а получено ${parts.size}") continue } val count = parts[1].toIntOrNull() if (count == null) { println(" «${parts[1]}» — не число") continue } cart.add(parts[0], count) .onSuccess { println(" ${parts[0]}: теперь $it шт.") } .onFailure { println(" Не добавлено: ${it.message}") } } println() println("В корзине ${cart.total()} шт.:") cart.items().forEach { (title, count) -> println(" $title × $count") } }

Настоящий вывод:

Вводите «название количество», пустая строка — конец.
> Nike 2
Nike: теперь 2 шт.
> Adidas 0
Не добавлено: количество должно быть больше нуля
> 3
Нужно два значения через пробел, а получено 1
> Puma 1
Puma: теперь 1 шт.
>

В корзине 3 шт.:
Nike × 2
Puma × 1
Нажми на часть выражения
Интересный факт

Посмотрите на класс Cart: в нём нет ни одного println. Он не знает, что его вызывают из консоли. Тот же класс без единой правки переносится в приложение — там его вызовет экран вместо main.

Это ровно та мысль, на которой позже строится вся мобильная разработка: логика отдельно, показ отдельно. В приложении роль main играет ViewModel, но принцип не меняется.

Отвечено вопросов: 0 из 3
Почему класс Cart не печатает результат сам?
Что делает Result.failure(…)?
Зачем в цикле нужен continue после сообщения об ошибке?
Отвечено верно: 0 из 3

Как запускать и отлаживать​

В Android Studio консольную программу запускают так же, как любую другую: зелёный треугольник напротив fun main(). Ввод набирается прямо в панели Run внизу.

Две вещи, которые экономят время:

  • Подать ввод файлом. Запуск вида программа < data.txt избавляет от набора одного и того же двадцать раз подряд. В настройках запуска Android Studio это поле «Redirect input from».
  • Заготовленный ввод в коде. Ровно тот приём, что использован в примерах этой главы: подменить чтение на список готовых строк. Так программа проверяется одним нажатием, а на настоящий readlnOrNull() переключается одной строкой.
Совет

Второй приём — это, по сути, первый шаг к тестам. Функция, которая получает строки откуда-то извне, а не читает их сама, проверяется автоматически. Функция с readlnOrNull() внутри — нет. Именно поэтому чтение выносят наружу, а не прячут в логике.

Типичные ошибки​

1. Проверка if (line == null) для пустого ввода. Enter даёт «», а не null. Нужна isBlank().

2. Разбор без trim(). « 42 ».toIntOrNull() — это null, и программа скажет «не число» про правильное число.

3. toInt() вместо toIntOrNull(). Опечатка пользователя роняет программу с NumberFormatException.

4. Ожидание запятой в дробном. «3,5».toDoubleOrNull() — null. Либо заменять запятую на точку перед разбором, либо честно просить точку.

5. Разложение split по переменным без проверки размера. IndexOutOfBoundsException на первой же короткой строке.

6. println вместо print для вопроса. Ответ вводится на следующей строке, и диалог выглядит рвано.

7. Форматирование без явной локали. На русской системе %.2f печатает запятую, и такое число уже не прочитать обратно.

8. println внутри класса с логикой. Работает в консоли и не переносится никуда дальше.

9. Ввод без подсказки. Программа молча ждёт, человек не понимает, чего от него хотят. Перед каждым чтением — вопрос.

Куда углубиться​

  • readLine() и кодировки: почему кириллица из файла иногда приходит «кракозябрами» и при чём тут Charsets.UTF_8.
  • Scanner из Java: почему в Kotlin он не нужен и чем nextInt() опасен.
  • Аргументы командной строки: fun main(args: Array<String>) — ввод, который не нужно спрашивать.
  • Разбор строки регулярным выражением, когда split уже не справляется.
  • Цветной вывод в терминале через ANSI-последовательности — и почему в Android Studio он иногда не работает.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоKotlin Input & Output Explained | println(), print(), readln() & User InputDev Trio
ВидеоKotlin Newbie To Pro - NULL VALUES AND USER INPUT - Part 10Philipp Lackner
ВидеоKotlin Tutorial: Interactive program (with user input)The Android Factory
ВидеоConsole Input Output | print, println, readln, String Templates | Kotlin Course #1.4ThinkProgramming-English
ВидеоУрок 10: Функции (методы), тип возвращаемого значения, return // Котлин курс с нуляKotlin & Android с нуля – Иван Ветров

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.