$ sudo teach IT

Когда заранее не известно, сколько раз повторять

Цикл 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.