JavaRush /Курсы /Kotlin SELF /Альтернативный цикл do-while

Альтернативный цикл do-while

Kotlin SELF
4 уровень , 2 лекция
Открыта

1. Введение

Иногда кажется, что while — это «универсальный цикл», и на нём можно остановиться. Но в реальных программах (даже самых простых консольных) часто встречается сценарий: действие должно произойти хотя бы один раз. Например, мы обязаны спросить у пользователя команду или число хотя бы один раз — иначе у нас просто нет данных, чтобы что-то проверять.

И вот тут while начинает заставлять писать «неуклюжий ритуал»: сначала выполнить действие один раз до цикла, а потом повторять это же действие внутри цикла. Это не смертельно, но раздражает — как когда вы дважды проверяете, закрыли ли дверь, и оба раза сомневаетесь.

do-while как раз и существует для таких случаев: «выполни тело минимум один раз, а потом уже смотри на условие».

С точки зрения языка Kotlin: у while проверка условия происходит до тела, а у do-whileпосле тела, поэтому тело do-while выполняется хотя бы один раз.

Синтаксис do-while и «человеческая» модель

Когда вы впервые видите do-while, он выглядит чуть необычно: условие не сверху, а снизу. Но логика очень простая: сначала делаем шаг, потом спрашиваем себя «ещё раз?».

Синтаксис:

do {
    // тело цикла
} while (condition)
Цикл do-while: сначала выполняем тело, потом проверяем условие повторения

Это буквально читается как фраза: «Сделай { … } и повторяй, пока 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
"abc"
null
null
true (повторяем)
2
"-5"
-5
null
true (повторяем)
3
"30"
30
30
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).

1
Задача
Kotlin SELF, 4 уровень, 2 лекция
Недоступна
Ник для чата
Ник для чата
1
Задача
Kotlin SELF, 4 уровень, 2 лекция
Недоступна
Подтверждение действия
Подтверждение действия
1
Задача
Kotlin SELF, 4 уровень, 2 лекция
Недоступна
Квадрат числа
Квадрат числа
1
Задача
Kotlin SELF, 4 уровень, 2 лекция
Недоступна
Чётный пропуск
Чётный пропуск
Комментарии (6)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Иван Уровень 11
30 апреля 2026
странным выглядит 4 "типичная ошибка" c do-while, причем тут trim() если абстрагироваться от контекста лекции, это наоборот частный случай, и в работе супер редко может использоваться
Andrey Уровень 6
15 марта 2026
Чтобы закрепить, вот пример «как нельзя» (он не скомпилируется): fun main() { do { val cmd = readln().trim() } while (cmd != "exit") // Ошибка: cmd не видна здесь } так компилируется ж и выполняется
11 мая 2026
Да уж, как-то подобные моменты расстраивают. В Java так нельзя, в Kotlin можно.
Victor Kharchenko Уровень 10
12 марта 2026
Задачки очень простые к сожалению, особенно первая...
Michael Уровень 28
14 марта 2026
Всё ещё впереди
11 мая 2026
Судя по курсу Java 25, вовсе не факт