$ sudo teach IT

Зачем нужен цикл

Представьте, что вам нужно поздороваться с каждым учеником в списке из тридцати имён или посчитать сумму чисел от одного до ста. Писать одну и ту же строку кода тридцать или сто раз — не вариант. Цикл решает именно эту задачу: он позволяет выполнить один и тот же блок кода для каждого элемента последовательности, не переписывая код руками.

В Swift для перебора последовательности используется цикл for-in. Официально это называется перебором (iteration) — вы как бы идёте по элементам один за другим и на каждом шаге выполняете тело цикла.

Обход диапазона

Диапазоны вы уже видели раньше. Цикл for-in умеет проходить по ним от начала до конца:

for i in 1...5 {
    print(i)
}
// напечатает числа от 1 до 5, каждое на новой строке

Здесь i — это временная константа: на каждом шаге цикла она получает следующее значение из диапазона 1...5, а тело цикла (то, что в фигурных скобках) выполняется заново. Объявлять i через let не нужно — цикл делает это сам, и внутри тела её нельзя менять.

Если вы используете полуоткрытый диапазон ..<, последнее число в переборе не участвует:

for i in 0..<3 {
    print("шаг \(i)")
}
// шаг 0, шаг 1, шаг 2 — тройки уже нет

Когда переменная счётчика не нужна

Иногда важно не само значение счётчика, а сам факт, что действие нужно повторить N раз. Заводить переменную, которую вы никогда не читаете, — плохой стиль: компилятор честно предупредит, что она не используется. Для таких случаев есть специальный символ подчёркивания _, который означает «мне это значение не нужно»:

for _ in 1...3 {
    print("Привет!")
}
// три раза напечатает "Привет!", номер повторения нас не интересует

Обход строки

Строка — это тоже последовательность, только не чисел, а символов Character. Цикл for-in проходит по строке символ за символом:

let word = "Swift"
for letter in word {
    print(letter)
}
// S, w, i, f, t — каждая буква на отдельной строке

На каждом шаге letter получает очередной символ строки. Это удобнее, чем доступ по индексам, о сложностях которого вы уже знаете: перебор через for-in сам разбирает строку на кластеры графем, ничего вручную считать не нужно.

Обратный порядок: reversed()

Метод reversed() разворачивает последовательность, и цикл проходит по ней от конца к началу:

for i in (1...3).reversed() {
    print(i)
}
// 3, 2, 1

Обратите внимание на скобки вокруг диапазона: сначала нужно построить диапазон 1...3, а уже потом развернуть результат — иначе компилятор не поймёт, к чему применить reversed(). То же самое работает и со строкой:

for letter in "abc".reversed() {
    print(letter)
}
// c, b, a

Шаг между значениями: stride

Диапазон 1...10 всегда идёт с шагом 1. Если нужен другой шаг — например, только чётные числа или обратный отсчёт, — на помощь приходит функция stride. У неё два варианта:

  • stride(from: a, to: b, by: step) — идёт от a и останавливается ПЕРЕД тем, как дойти до b (граница b в перебор не попадает, как у ..<);
  • stride(from: a, through: b, by: step) — идёт от a и включает b, если шаг попадает точно на неё (граница включена, как у ...).
for i in stride(from: 0, to: 10, by: 2) {
    print(i)
}
// 0, 2, 4, 6, 8 — до десяти, саму десятку не включает

for i in stride(from: 0, through: 10, by: 2) {
    print(i)
}
// 0, 2, 4, 6, 8, 10 — десятка включена

Шаг by может быть и отрицательным — тогда перебор идёт в обратную сторону:

for i in stride(from: 10, through: 0, by: -5) {
    print(i)
}
// 10, 5, 0

Индекс и значение вместе: enumerated()

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

let letters = "abc"
for (index, letter) in letters.enumerated() {
    print("\(index): \(letter)")
}
// 0: a
// 1: b
// 2: c

Индекс здесь всегда начинается с нуля и просто отсчитывает порядковый номер элемента — это не связано со значениями, которые вы перебираете.

Вложенные циклы

Цикл может содержать внутри себя ещё один цикл — это называется вложенным циклом. Он полезен, когда для каждого элемента внешней последовательности нужно перебрать ещё одну последовательность. Классический пример — таблица умножения, где для каждой строки нужно перебрать все столбцы:

for row in 1...3 {
    var line = ""
    for column in 1...3 {
        line += "\(row * column) "
    }
    print(line)
}
// 1 2 3
// 2 4 6
// 3 6 9

Внешний цикл выбирает строку, а внутренний на каждом шаге внешнего проходит по всем столбцам заново — целиком, с самого начала. На каждую строку внутренний цикл отрабатывает полностью, поэтому три строки внешнего цикла дают в сумме девять срабатываний внутреннего.

Условие внутри цикла: where

Если из всей последовательности нужны только элементы, которые соответствуют условию, можно добавить where прямо в заголовок for-in — тогда тело цикла выполнится только для тех значений, где условие истинно, а остальные будут пропущены:

for i in 1...10 where i % 2 == 0 {
    print(i)
}
// 2, 4, 6, 8, 10 — только чётные

Это короче и читается яснее, чем обернуть всё тело цикла в if. Условие в where проверяется на каждом шаге перед тем, как цикл войдёт в тело.

Частые ошибки

Диапазон 1...n упадёт с ошибкой во время выполнения, если n окажется меньше 1 — нижняя граница диапазона не может быть больше верхней. Перед тем как строить диапазон из переменных величин, стоит убедиться, что он действительно корректен.

Компилятор предупредит, если вы объявили переменную цикла, но ни разу её не используете внутри тела, — в таком случае замените её на _.

Ошибка в порядке границ stride: если from больше конечной границы, а шаг by положительный (или наоборот, from меньше границы, а шаг отрицательный), перебор просто не выполнится ни разу — программа не упадёт, но и работать не будет так, как вы ожидали.

Путаница между to и through в stride — частая причина, почему последнее значение «пропадает» или, наоборот, лишний раз появляется в переборе. Если граница обязана попасть в перебор, нужен именно through.

Итоги

  • for-in перебирает диапазоны, строки и другие последовательности элемент за элементом
  • _ используется вместо переменной счётчика, когда её значение не нужно
  • reversed() разворачивает последовательность перед перебором
  • stride(from:to:by:) не включает конечную границу, stride(from:through:by:) включает
  • enumerated() даёт пары «индекс, элемент», которые удобно разбирать как кортеж
  • вложенный цикл на каждом шаге внешнего запускает внутренний цикл целиком заново
  • where в заголовке for-in пропускает шаги, не подходящие под условие

Проверьте себя

3 вопроса

Факториал числа

Напишите функцию factorial(_:), которая принимает целое число n (всегда 1 или больше) и возвращает его факториал — произведение всех целых чисел от 1 до n включительно.

Используйте цикл for-in по диапазону от 1 до n и накапливайте произведение в переменной. Например, для n = 5 результат — 120, потому что 1 * 2 * 3 * 4 * 5 = 120.

Количество цифр в строке

Напишите функцию countDigits(_:), которая принимает строку text и возвращает количество символов-цифр в ней.

Обойдите строку циклом for-in по символам и для каждого символа проверяйте, является ли он цифрой, через свойство isNumber у Character. Пустая строка и строка без единой цифры должны дать 0.

Сумма с шагом

Напишите функцию sumStride(from:through:by:), которая складывает все числа от from до through включительно с шагом step, используя stride(from:through:by:).

Например, при from = 1, through = 10, step = 2 нужно сложить 1, 3, 5, 7 и 9, получив 25. Гарантируется, что шаг подобран так, что перебор в нужном направлении происходит хотя бы один раз.