Когда заранее не известно, сколько раз повторять
Цикл for-in хорош, когда вы заранее знаете, по чему проходите: по диапазону чисел, по символам строки, по последовательности из stride. Но бывают задачи другого рода: вы повторяете действие, пока не выполнится какое-то условие, а сколько именно раз это займёт — заранее неизвестно. Делить число на 10, пока оно не станет нулём. Запрашивать у пользователя число, пока он не введёт корректное. Подбрасывать монету, пока не выпадет орёл. В каждом случае число повторений зависит от данных, а не от заранее известной последовательности — под это в Swift есть отдельный вид циклов.
while: пока условие верно
Цикл while проверяет условие перед каждым повторением тела. Пока условие — true, тело выполняется снова; как только оно становится false, цикл останавливается и выполнение идёт дальше.
var attempts = 0
while attempts < 3 {
print("Попытка \(attempts)")
attempts += 1
}
print("Попытки закончились")
Здесь условие attempts < 3 проверяется перед каждым входом в тело. Пока оно верно, счётчик увеличивается и печатается сообщение; как только attempts становится равно 3, условие превращается в false, и цикл завершается, не выполнив тело в четвёртый раз. Обратите внимание: если бы attempts изначально был равен 3, тело while не выполнилось бы вообще ни разу — условие проверяется до первого прохода.
repeat-while: сначала тело, потом проверка
Иногда действие в любом случае нужно выполнить хотя бы один раз, а уже потом решать, повторять ли его снова — например, показать пользователю меню и переспрашивать, пока он не введёт понятную команду. Для этого есть repeat-while: тело выполняется первым, а условие проверяется уже после него, в конце.
var code = 91
repeat {
print("Обрабатываем код \(code)")
code -= 30
} while code > 100
Условие code > 100 изначально ложно, но тело всё равно выполнится один раз — это и есть ключевое отличие от while: repeat-while гарантирует минимум одно выполнение тела, а обычный while может не выполнить тело ни разу. Выбирайте repeat-while, когда по смыслу задачи «сделать хотя бы раз, а там видно» — во всех остальных случаях привычнее и понятнее обычный while.
break и continue: вмешательство в ход цикла
Оба ключевых слова работают одинаково в while, repeat-while и уже знакомом for-in — разница между ними в том, насколько сильно они прерывают цикл.
continue прекращает только текущий проход тела и сразу переходит к следующей проверке условия (для for-in — к следующему элементу), не выполняя оставшийся код тела на этом шаге:
var sum = 0
for i in 1...5 {
if i % 2 == 0 {
continue
}
sum += i
}
print(sum) // 9
Чётные числа 2 и 4 пропускаются через continue ещё до строки sum += i, поэтому в сумму попадают только нечётные: 1 + 3 + 5 = 9.
break действует радикальнее — он немедленно останавливает весь цикл целиком, и выполнение продолжается со строки сразу после него:
var index = 0
while index < 1000 {
if index == 4 {
break
}
index += 1
}
print(index) // 4
Формальная верхняя граница 1000 здесь не имеет значения: как только index становится равен 4, срабатывает break, и цикл заканчивается сразу же, не дожидаясь провала условия index < 1000.
Метки циклов: выход сразу из нескольких вложенных циклов
У вложенных циклов есть особая сложность: обычный break внутри внутреннего цикла останавливает только его, а внешний цикл продолжает крутиться дальше. Если нужно выйти сразу из всех уровней вложенности, циклу дают метку — произвольное имя перед ключевым словом цикла — и указывают эту метку у break или continue:
outer: for i in 1...3 {
for j in 1...3 {
if j == 2 {
break outer
}
print(i, j)
}
}
Метка outer ставится перед внешним for-in через двоеточие. Как только внутренний цикл доходит до j == 2, срабатывает break outer — он останавливает не только внутренний цикл, но и внешний, помеченный этой меткой. Программа напечатает только 1 1: без метки, просто break, остановил бы лишь внутренний цикл, и внешний перешёл бы к i = 2, снова печатая по одной паре. Такая же метка работает и с continue outer — она перейдёт к следующей итерации именно внешнего цикла, минуя оставшуюся часть внутреннего.
Как не уйти в бесконечный цикл
У while и repeat-while, в отличие от for-in по готовому диапазону, нет встроенной гарантии, что условие когда-нибудь станет ложным — об этом целиком заботится ваш код. Бесконечный цикл получается ровно по одной причине: то, что участвует в условии, не меняется внутри тела так, чтобы условие рано или поздно перестало выполняться. Возьмите за правило: у каждого while должна быть переменная, которая шагает к завершающему значению на каждом проходе, и её изменение должно быть в теле цикла безусловно, а не только внутри какого-то if, который может не сработать. В песочнице курса это не просто теория: если программа не заканчивается сама за отведённое время, её принудительно останавливают по таймауту, и задача засчитывается как проваленная — даже если логика внутри цикла в остальном верна.
Типичный приём: разбор числа по цифрам
Классическая задача, где число повторений заранее не знает никто — обработать число поразрядно, когда неизвестно, сколько в нём цифр. Стандартный приём — делить число на 10 в цикле, пока оно не станет нулём: остаток от деления на 10 даёт очередную цифру справа, а целочисленное деление на 10 отбрасывает эту цифру и сдвигает всё на разряд.
var number = 4507
var sum = 0
while number > 0 {
let digit = number % 10
sum += digit
number /= 10
}
print(sum) // 16
На каждом шаге number % 10 достаёт последнюю цифру, она добавляется к сумме, а затем number /= 10 отбрасывает эту цифру у самого числа. Цикл сам собой останавливается, когда от числа ничего не остаётся — number становится равен 0, и условие number > 0 перестаёт выполняться. Этот же приём, только с накоплением результата в другом месте, лежит в основе разворота числа, подсчёта цифр и множества похожих задач.
Частые ошибки
- Забыть изменить переменную, от которой зависит условие — цикл получится бесконечным, а в песочнице такая задача просто оборвётся по таймауту.
- Спрятать изменение переменной внутрь
if, который выполняется не всегда — условие цикла может стать ложным не для всех входных данных, и бесконечный цикл проявится только на части тестов. - Взять
repeat-whileтам, где нужен обычныйwhile— если условие изначально ложно, а действие всё равно не должно случиться ни разу,repeat-whileего всё равно выполнит один раз. - Спутать
breakиcontinue—continueлишь пропускает остаток текущего прохода, аbreakостанавливает цикл целиком; если нужно просто пропустить один случай, а не выйти из цикла, нуженcontinue. - Поставить обычный
breakво внутреннем цикле, ожидая, что он остановит и внешний — без метки он остановит только тот цикл, в котором написан.
Резюме
while условие { }проверяет условие перед каждым проходом и может не выполнить тело ни разу.repeat { } while условиевыполняет тело сначала, а условие проверяет потом — тело гарантированно отработает хотя бы один раз.continueпрерывает только текущий проход и переходит к следующему;breakостанавливает весь цикл целиком.- Метка перед циклом (
outer: for ...) вместе сbreak outerилиcontinue outerпозволяет управлять сразу внешним циклом из вложенного. - Ответственность за завершение
whileиrepeat-whileцеликом на вашем коде: переменная в условии обязана меняться безусловно на каждом проходе, иначе цикл не остановится, а песочница прервёт его по таймауту. - Деление на 10 в цикле
while— стандартный способ разобрать число по цифрам, когда их количество заранее неизвестно.
Проверьте себя
3 вопроса
Сумма цифр числа
Напишите функцию digitSum(_ n: Int) -> Int, которая возвращает сумму цифр неотрицательного числа n.
Используйте цикл while: на каждом шаге берите последнюю цифру числа через остаток от деления на 10, прибавляйте её к сумме, а затем отбрасывайте эту цифру у числа целочисленным делением на 10. Для n, равного 0, функция должна вернуть 0.
Разворот числа
Напишите функцию reverseNumber(_ n: Int) -> Int, которая переворачивает цифры неотрицательного числа n задом наперёд. Например, из 123 должно получиться 321.
Если число оканчивается нулями, в перевёрнутом числе эти нули просто исчезают: из 120 получается 21, а не 021. Для n, равного 0, функция должна вернуть 0.
Первое общее кратное в диапазоне
Напишите функцию firstDivisibleByBoth(from start: Int, to end: Int, x: Int, y: Int) -> Int. Она должна найти наименьшее число в диапазоне от start до end включительно, которое делится без остатка одновременно и на x, и на y.
Переберите числа от start до end в цикле while и, как только найдёте подходящее число, сразу выйдите из цикла через break — не нужно проверять оставшиеся числа, если ответ уже найден. Если во всём диапазоне такого числа нет (в том числе если start больше end), функция должна вернуть -1.