1. Введение
Иногда кажется, что while — это «универсальный цикл», и на нём можно остановиться. Но в реальных программах (даже самых простых консольных) часто встречается сценарий: действие должно произойти хотя бы один раз. Например, мы обязаны спросить у пользователя команду или число хотя бы один раз — иначе у нас просто нет данных, чтобы что-то проверять.
И вот тут while начинает заставлять писать «неуклюжий ритуал»: сначала выполнить действие один раз до цикла, а потом повторять это же действие внутри цикла. Это не смертельно, но раздражает — как когда вы дважды проверяете, закрыли ли дверь, и оба раза сомневаетесь.
do-while как раз и существует для таких случаев: «выполни тело минимум один раз, а потом уже смотри на условие».
С точки зрения языка Kotlin: у while проверка условия происходит до тела, а у do-while — после тела, поэтому тело do-while выполняется хотя бы один раз.
Синтаксис do-while и «человеческая» модель
Когда вы впервые видите do-while, он выглядит чуть необычно: условие не сверху, а снизу. Но логика очень простая: сначала делаем шаг, потом спрашиваем себя «ещё раз?».
Синтаксис:
do {
// тело цикла
} while (condition)
Это буквально читается как фраза: «Сделай { … } и повторяй, пока condition истинно».
Важно запомнить одно правило, которое потом спасает от путаницы: в do-while условие отвечает за повторение следующей итерации, а не за разрешение первой. Первая итерация будет всегда — потому что «сначала сделать» уже случилось.
2. while и do-while на одной задаче
Очень полезно увидеть разницу не «в теории», а на одном и том же сценарии. Возьмём задачу: пользователь должен ввести слово "exit", чтобы закончить. Мы обязаны спросить его хотя бы один раз.
Вариант на while с дублированием
fun main() {
print("Введите команду (exit для выхода): ")
var cmd = readln().trim().lowercase()
while (cmd != "exit") {
print("Введите команду (exit для выхода): ")
cmd = readln().trim().lowercase()
}
println("Завершение.") // Завершение.
}
Этот код работает, но обратите внимание: строка с print(...) и строка с чтением readln() встречаются дважды. Новички иногда говорят: «Ну и ладно». А потом наступает момент, когда вы меняете текст подсказки, и забываете поменять его в одном месте. И программа начинает вести себя так, будто у неё раздвоение личности.
Вариант на do-while без дублей
fun main() {
var cmd: String
do {
print("Введите команду (exit для выхода): ")
cmd = readln().trim().lowercase()
} while (cmd != "exit")
println("Завершение.") // Завершение.
}
Тот же смысл, но «ритуал дублирования» исчез. Это и есть главный практический бонус do-while.
3. Повторный ввод числа без дублирования
Сейчас сделаем ровно то, ради чего do-while особенно любят в консольных программах: устойчивый ввод «до тех пор, пока не введут нормально».
Пусть нам нужно число от 1 до 5. Пользователь может ввести что угодно: 3, " 3 ", "три", пустую строку, 999. Наша программа должна оставаться спокойной и просто повторять запрос.
Здесь мы используем уже знакомый паттерн: toIntOrNull() даёт Int?, то есть либо число, либо null.
Ввод числа в диапазоне 1..5 через do-while
fun main() {
var number: Int? = null
do {
print("Введите целое число от 1 до 5: ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
number = if (parsed != null && parsed in 1..5) parsed else null
if (number == null) println("Некорректно.") // Некорректно.
} while (number == null)
println("Принято: $number") // Принято: 4
}
Здесь есть важная идея, которая делает код читаемым: переменная number хранит «принятое» значение. Пока не принято — там null. Цикл повторяется, пока number == null.
4. Область видимости в do-while
Когда вы пишете do-while, возникает вполне нормальный вопрос: «А где объявлять переменную, которую проверяем в условии?». Ответ: так, чтобы она была видна и в теле, и в условии while (...).
Поэтому такой шаблон встречается постоянно:
fun main() {
var cmd: String
do {
cmd = readln().trim()
} while (cmd != "exit")
}
Если объявить val cmd внутри блока do { ... }, то снаружи (в while (cmd != ...)) она уже не будет видна — и компилятор скажет «не знаю такого имени». Это не каприз Kotlin, это обычное правило области видимости: имя живёт внутри блока, где объявлено.
Чтобы закрепить, вот пример «как нельзя» (он не скомпилируется):
fun main() {
do {
val cmd = readln().trim()
} while (cmd != "exit") // Ошибка: cmd не видна здесь
}
5. Полезные нюансы do-while
Мини-схема: как do-while выполняется по шагам
Чтобы меньше путаться, полезно держать в голове маленькую схему выполнения. Представим, что у нас цикл повторного ввода числа (пока number == null).
flowchart TD
A["START"] --> B["do { ... } выполняется (минимум 1 раз)"]
B --> C{"условие while (...)?"}
C -->|true| B
C -->|false| D["цикл завершён"]
D --> E["END"]
Сравните с while: там проверка до тела, поэтому возможен сценарий «0 итераций». А у do-while — не возможен: первая итерация всегда будет.
Как писать читаемое условие
do-while легко превратить в монстра, если писать в while (...) условие из пяти проверок со скобками, &&, || и попытками «всё уместить в одну строку». На этом этапе полезно помнить: читаемость — это часть корректности.
Если условие становится тяжёлым, проще сделать так: в теле цикла посчитать «принято/не принято» в отдельную переменную, и уже её поставить в while.
Вот пример того же возраста, но чуть более «разговорно»:
fun main() {
var age: Int? = null
do {
print("Возраст (0..120): ")
val parsed = readln().trim().toIntOrNull()
val ok = parsed != null && parsed in 0..120
age = if (ok) parsed else null
if (!ok) println("Введите целое число от 0 до 120.")
} while (age == null)
println("Возраст принят: $age") // Возраст принят: 42
}
Код стал длиннее на одну строчку, но мозгу легче.
Микро-разбор по итерациям: что происходит с переменными
Иногда полезно «прокрутить» цикл в голове, как будто вы отлаживаете программу вручную. Возьмём ввод возраста и предположим, что пользователь ввёл: "abc", потом "-5", потом "30".
Внутри тела на каждой итерации происходит примерно это:
| Итерация | raw | parsed | age после присваивания | Условие age == null |
|---|---|---|---|---|
| 1 | |
|
|
true (повторяем) |
| 2 | |
|
|
true (повторяем) |
| 3 | |
|
|
false (выходим) |
То есть do-while тут делает очень понятную работу: «пытайся принять значение, пока не получится».
6. Анкета v0.4: имя и возраст
Чтобы примеры не были набором отдельных кусочков, давайте будем развивать одно мини-приложение. Пусть это будет «анкета»: пользователь вводит имя и возраст. Мы не делаем функции (кроме main) и не используем массивы — всё по уровню текущего дня.
Идея такая: имя должно быть не пустым (после trim()), а возраст — целое число от 0 до 120. Мы хотим минимально «доставать» пользователя, но при этом не принимать мусор.
Шаг 1: надёжный ввод имени
fun main() {
var name: String
do {
print("Введите имя: ")
name = readln().trim()
if (name.isEmpty()) println("Имя не должно быть пустым.") // Имя не должно быть пустым.
} while (name.isEmpty())
println("Привет, $name!") // Привет, Анна!
}
Обратите внимание: здесь у нас нет null, потому что readln() всегда возвращает строку. Нам достаточно проверять isEmpty() после trim().
Шаг 2: надёжный ввод возраста
Теперь добавим возраст. Здесь мы используем Int?, потому что toIntOrNull() может вернуть null.
fun main() {
var name: String
do {
print("Введите имя: ")
name = readln().trim()
if (name.isEmpty()) println("Имя не должно быть пустым.")
} while (name.isEmpty())
var age: Int? = null
do {
print("Введите возраст (0..120): ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
age = if (parsed != null && parsed in 0..120) parsed else null
if (age == null) println("Возраст введён некорректно.") // Возраст введён некорректно.
} while (age == null)
println("Пользователь: $name, возраст: $age") // Пользователь: Анна, возраст: 25
}
Технически мы могли бы хранить age как Int и отдельно флаг «валидно/невалидно», но на текущем этапе Int? часто получается проще: null = «ещё не приняли».
7. Типичные ошибки при работе с do-while
Ошибка №1: ожидать, что цикл может не выполниться ни разу.
Это привычка от while. У do-while тело выполнится минимум один раз всегда, даже если условие изначально «ложное». Поэтому do-while не подходит для ситуаций, где действие может быть не нужно вообще. Если вам важно «0 итераций — это нормально», чаще подходит while.
Ошибка №2: объявить переменную внутри do {}, а использовать в while (...).
Это классическая проблема области видимости. Если переменная объявлена внутри блока, снаружи она не видна, и условие не скомпилируется. Почти всегда переменную, участвующую в условии, объявляют до цикла, чтобы она была доступна в обоих местах.
Ошибка №3: использовать toInt() вместо toIntOrNull() в сценарии повторного ввода.
toInt() при вводе вроде "abc" завершит программу исключением, а нам в учебных сценариях нужно наоборот — мягко сказать «некорректно» и повторить. Для устойчивого ввода лучше toIntOrNull(), потому что он превращает нечисловой ввод в null.
Ошибка №4: забыть нормализовать строку (trim()) перед проверками.
Пользователь легко введёт " exit " или " 25 ". Без trim() такие значения часто «не совпадут» с ожидаемыми, и программа будет упрямо делать вид, что пользователь ошибается, хотя он просто поставил пробел. Это особенно заметно в командах ("exit") и числах.
Ошибка №5: слишком сложное условие в while (...) и потеря смысла.
Если вы пишете внизу цикла выражение на полэкрана, вы почти гарантированно однажды ошибётесь в скобках или логике. На этом уровне лучше считать условие «принято/не принято» внутри тела в отдельный val ok и затем управлять повторением через понятную проверку вроде while (!ok) или while (age == null).
Ошибка №6: смешивать в одной переменной разные роли.
Например, пытаться хранить и «сырую строку ввода», и «число», и «принято ли значение» в одном var x. Получается путаница, а потом — баги, которые сложно объяснить даже себе. Лучше: raw для строки, parsed для результата парсинга, и отдельная переменная для принятого значения (age) или для флага (ok).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ