1. Введение
Представьте, что вы пишете программу, которая просит пользователя ввести возраст. Пользователь вводит "abc". Вы говорите "некорректно". И… что дальше? Если у вас только if, программа просто доходит до конца и заканчивается, оставляя пользователя с ощущением «ну и ладно тогда». Циклы нужны ровно для этого: научить программу повторять действие, пока не будет достигнут нужный результат — или пока условие истинно.
В реальной жизни циклы встречаются везде: повторный ввод, подсчёты «от 1 до N», накопление суммы, постепенное приближение к цели. И да, циклы — главный инструмент, которым можно случайно устроить бесконечность (программисты иногда тоже любят вечность, но компилятор — не всегда).
2. while: синтаксис и ментальная модель
Цикл while читается очень по-человечески: «пока условие истинно — делай». В Kotlin он выглядит так:
while (condition) {
// тело цикла
}
Главная особенность: условие проверяется до входа в тело. Это значит, что если условие сразу false, тело не выполнится ни разу — и это не «ошибка», а полезное поведение. Именно поэтому while идеально подходит для ситуаций «выполняем, пока есть смысл».
while — это как охранник на входе в клуб: «Есть билет?» (условие). Если билета нет, внутрь не пустят вообще ни на одну итерацию.
Небольшая блок-схема, чтобы увидеть движение программы глазами:
flowchart TD
A[Старт] --> B{condition?}
B -- нет --> E[Выход из цикла]
B -- да --> C[Тело цикла]
C --> D[Конец итерации]
D --> B
И да: Kotlin — это Kotlin, но while тут абсолютно классический, «как везде», без сюрпризов. Даже в официальных примерах императивного стиля (где используются var и цикл) while выглядит именно так.
3. Паттерны while
Счётчик: три обязательные части
Когда новички видят цикл, первая мысль часто такая: «О, повторение! Сейчас я повторю что-нибудь… и оно само остановится». Не остановится. Циклу нужно помочь.
У классического цикла-счётчика есть три части, и они почти всегда видны прямо в коде:
- начальное значение (до цикла),
- условие продолжения (в while (...)),
- изменение переменной, чтобы условие когда-нибудь стало ложным (внутри тела).
Самый простой пример — печатаем числа от 1 до 5:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i") // i = 1, потом 2, 3, 4, 5
i = i + 1
}
}
Обратите внимание: переменная i объявлена как var, потому что мы её меняем. Если бы это было val, компилятор справедливо сказал бы: «вы же обещали не менять».
Теперь пример с границей, где тело может не выполниться ни разу. Это очень важная «проверка на честность» для вашего понимания:
fun main() {
var i = 10
while (i <= 5) {
println("Это не напечатается")
i = i + 1
}
println("Цикл не запускался ни разу") // Цикл не запускался ни разу
}
Условие i <= 5 сразу false, поэтому внутрь цикла мы не попали. Иногда это ровно то, что нужно: например, «повторять, пока есть ошибки», а ошибок может не быть уже на старте.
Накопление: сумма и максимум
Цикл while полезен не только для печати «шагов», но и для накопления результата. Типовой сценарий: «есть несколько чисел, нужно посчитать сумму». Пока у нас нет массивов и списков, мы тренируемся на фиксированном количестве значений или на «вводим N раз».
Начнём с совсем маленького: сумма от 1 до 3.
fun main() {
var sum = 0
var i = 1
while (i <= 3) {
sum = sum + i
i = i + 1
}
println("sum = $sum") // sum = 6
}
Здесь две роли переменных очень чёткие: i — «шаг», sum — «накопитель». Когда вы смешиваете роли (например, используете одну переменную и как счётчик, и как сумму), код становится похож на шутку, которая смешная только автору.
Ещё один полезный паттерн — находить максимум среди нескольких введённых значений. Да, позже мы сделаем это гораздо удобнее, но сейчас нам важна логика «сравниваем и обновляем».
Допустим, мы хотим обработать 3 числа (ввод считаем корректным, чтобы не мешать теме):
fun main() {
var max: Int? = null
var count = 0
while (count < 3) {
print("Введите число: ")
val n = readln().trim().toInt()
if (max == null || n > max) max = n
count = count + 1
}
println("max = $max") // например: max = 42
}
Здесь max сделан nullable (Int?), потому что «максимума» до первого ввода ещё нет. Это хороший пример того, что nullable-типы — не только для «страшных тем про null», а иногда просто честный способ сказать: «значение появится позже».
Бесконечный цикл: почему он возникает и как его вычислить
Пора поговорить о главном развлечении начинающего программиста: «я запустил программу, и она зависла». На самом деле она часто не зависла — она просто нашла смысл жизни и теперь бесконечно повторяет одно и то же.
Причина бесконечного цикла почти всегда одна: условие не становится false. Либо вы забыли изменить переменную, либо меняете не ту, либо меняете «не туда».
Самая классическая ошибка:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i")
// забыли i = i + 1
}
}
Тут i всегда 1, условие всегда истинно, и программа будет печатать "i = 1" до тех пор, пока вы (или ваша операционная система) не скажете: «всё, хватит».
Ещё более коварная версия — вы обновляете переменную, но так, что условие никогда не станет ложным:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i")
i = i - 1
}
}
i уходит в 0, -1, -2… и условие i <= 5 остаётся истинным навсегда.
Практическое правило «анти-бесконечность» звучит скучно, но работает железно: после написания while глазами найдите в теле строку, которая влияет на условие. Если не нашли — у вас почти наверняка вечный двигатель.
4. Повторный ввод и проверка
Счётчики — это хорошо, но одна из самых жизненных причин полюбить while — устойчивый ввод. Пользователь может ввести пробелы, пустую строку, «сорок два» буквами и даже "42.0" (а вы хотели Int). И если вы используете toInt(), программа может упасть. Поэтому для сценария «повторять ввод до корректного значения» мы используем связку trim() + toIntOrNull().
readln() читает строку целиком, а toIntOrNull() возвращает либо число, либо null, если строка не число. Это идеально ложится на while.
Пример: просим возраст (целое число от 0 до 130) и повторяем, пока не получим нормальный результат.
fun main() {
var age: Int? = null
while (age == null) {
print("Введите возраст (0..130): ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
if (parsed != null && parsed in 0..130) {
age = parsed
} else {
println("Некорректный ввод. Попробуйте ещё раз.")
}
}
println("Возраст принят: $age") // например: Возраст принят: 25
}
Почему это удобно:
- Мы не «падаем» на плохом вводе — потому что не используем toInt() напрямую.
- Условие цикла очень читаемое: «пока возраст не получен».
- Внутри цикла явно видно, что именно делает ввод корректным: число должно распарситься и попасть в диапазон.
Заметьте важную мелочь: проверка parsed in 0..130 возможна только если parsed != null. Поэтому мы пишем parsed != null && ... Это продолжение той же логики, которую вы уже делали в условиях с && и ||.
5. Мини‑приложение: BudgetBuddy v0
Сейчас соберём небольшой цельный сценарий, который будет выглядеть как «настоящая программа», а не набор разрозненных примеров. Пусть это будет BudgetBuddy v0: мини-помощник, который просит ввести три траты и считает сумму. Главный фокус — цикл while и устойчивость к неправильному вводу без break/continue (их мы ещё не проходили).
Идея такая: нам нужно ровно 3 корректных числа. Если пользователь ввёл ерунду — мы не двигаем счётчик и просим снова. Это очень важный приём: счётчик увеличивается только при успехе.
fun main() {
var sum = 0
var entered = 0
while (entered < 3) {
print("Введите расход №${entered + 1} (целое число >= 0): ")
val raw = readln().trim()
val value = raw.toIntOrNull()
if (value != null && value >= 0) {
sum = sum + value
entered = entered + 1
} else {
println("Некорректно. Нужно целое число >= 0.")
}
}
println("Сумма 3 расходов = $sum") // например: Сумма 3 расходов = 1200
}
Обратите внимание, как аккуратно распределились роли:
entered отвечает только за то, сколько корректных расходов мы приняли. Если ввод плохой, entered не меняется, и мы остаёмся на том же «номере расхода». Это и есть «повторный ввод» в реальной жизни, без дублирования кода.
И ещё одна деталь про читаемость: "№${entered + 1}" — это строковый шаблон, который вы уже знаете. Он делает подсказку дружелюбной: пользователь понимает, что вводит «расход №2», а не просто какие-то абстрактные числа в пустоту.
6. Типичные ошибки при работе с while
Ошибка №1: забыли изменить переменную, от которой зависит условие.
Самая частая причина бесконечного цикла — переменная в условии (i, entered, age) остаётся неизменной. Полезная привычка: сразу после написания while (...) глазами найти в теле строку, которая делает условие когда-нибудь ложным. Если такой строки нет — цикл, скорее всего, вечный.
Ошибка №2: обновляете «не туда» и условие никогда не станет ложным.
Иногда обновление есть, но логика неверная: например, в цикле while (i <= 5) вы делаете i = i - 1. Формально программа «что-то меняет», но по смыслу она уходит в сторону, где условие остаётся истинным бесконечно. Когда замечаете, что значение «убегает» от границы — остановитесь и проверьте знак изменения.
Ошибка №3: смешали роли переменных, и цикл перестал быть понятным.
Если одной переменной вы и считаете шаги, и храните сумму, и ещё пытаетесь держать «последний ввод», код становится трудно читать и легко сломать. Хороший стиль для новичка — «одна переменная = одна роль»: отдельно счётчик, отдельно накопитель, отдельно распарсенное значение.
Ошибка №4: используете toInt() в цикле ввода и ловите падения программы.
toInt() хорош, когда вы уверены в формате ввода. Но в задачах «ввод от пользователя» уверенности обычно нет. Поэтому для повторного ввода лучше использовать toIntOrNull() и проверку на null, как мы сделали выше: так программа не падает и может вежливо попросить повторить ввод.
Ошибка №5: сравниваете или проверяете диапазон для значения, которое может быть null.
Типичная ситуация: val x = raw.toIntOrNull() и дальше попытка сделать if (x >= 0). Компилятор Kotlin не даст так написать (и будет прав), потому что x может быть null. Правильный порядок мыслей — сначала «не null?», потом «подходит ли диапазон?». Это особенно важно в циклах, потому что ошибка в проверке часто превращается в бесконечное повторение или в невозможность принять корректный ввод.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ