Зачем это знать
Вы уже работали с массивом, множеством, словарём, строкой и диапазоном. У них есть count, у всех можно вызвать map, filter, contains, sorted, все можно перебрать циклом for. Это не совпадение и не то, что автора языка каждый раз заставляли переписывать одни и те же методы пять раз для пяти типов. Все эти методы объявлены ОДИН раз — на уровне протокола, которому подчиняются сразу все перечисляемые типы. Понимание этого убирает ощущение «в Swift слишком много методов, которые надо запомнить»: на самом деле их немного, просто они достаются любому новому типу бесплатно, если он согласится соответствовать нужному протоколу.
Sequence: то, что можно перебрать
Протокол Sequence — это официальное название для «набора значений, которые можно пройти по очереди, один за другим». Именно из-за соответствия Sequence у массива, множества, словаря, диапазона и строки работает цикл for value in something. Массив соответствует Sequence, множество соответствует Sequence, диапазон соответствует Sequence — и поэтому for выглядит одинаково для всех.
Sequence также приносит с собой большую пачку готовых методов, которые вы уже использовали: map, filter, reduce, compactMap, contains, sorted, min, max, first(where:). Все они написаны один раз в стандартной библиотеке для протокола Sequence и работают через одну и ту же базовую операцию — «дай следующий элемент». Если тип умеет отдавать элементы по одному, он бесплатно получает десятки готовых методов сверху.
Collection: то же самое, но многократно и по индексу
У чистого Sequence есть ограничение: гарантируется только однократный проход. Формально протокол не обещает, что после того как вы дошли до конца, можно начать заново с того же значения. На практике массивы, множества, словари и строки перебираются сколько угодно раз — потому что они соответствуют более сильному протоколу, Collection.
Collection расширяет Sequence и добавляет то, что вы тоже уже использовали не задумываясь: обращение по индексу через collection[index], свойство count без обхода всех элементов заново, isEmpty, first. У массива count мгновенный, потому что Array хранит длину отдельно — это часть контракта Collection, а не случайность реализации.
Соотношение простое: каждый Collection — это Sequence, но не каждый Sequence — это Collection. Диапазон, массив, множество, словарь и строка — и то, и другое. А вот, например, поток данных, который читается один раз и не хранится в памяти целиком, может быть только Sequence.
Итератор: как на самом деле устроен перебор
Когда вы пишете for value in numbers, компилятор Swift разворачивает это в более простой код. Он просит у последовательности итератор — специальный маленький объект, который умеет отвечать на один вопрос: «какое следующее значение и есть ли оно вообще». Это описано протоколом IteratorProtocol с единственным методом next().
next() возвращает опционал: пока есть очередной элемент — приходит Optional с этим значением, а когда элементы кончились — приходит nil. Это ровно то же самое опциональное «есть значение или нет», которое встречалось раньше. Вот перебор массива вручную, без единого for:
let numbers = [10, 20, 30]
var iterator = numbers.makeIterator()
while let value = iterator.next() {
print(value)
}
Метод makeIterator() — часть протокола Sequence — создаёт новый итератор. Дальше цикл while let раз за разом спрашивает next(): получает значение, печатает, спрашивает снова. Как только next() вернёт nil, разворачивание не удастся, и while let завершится. Обычный for value in numbers делает ровно это, только компилятор пишет цикл за вас.
Ленивые последовательности: lazy
По умолчанию каждый вызов map или filter на массиве сразу же строит новый массив целиком. Если вызовов несколько подряд, каждый создаёт промежуточный массив, даже если в итоге нужен всего один элемент. Модификатор lazy меняет это поведение: он превращает цепочку в последовательность, которая вычисляет каждое значение только тогда, когда его действительно запросили.
let big = Array(1...1_000_000)
let firstBigSquare = big.lazy.map { $0 * $0 }.first { $0 > 50 }
// вычислит квадраты только первых восьми чисел, а не всего миллиона
Без lazy map посчитал бы миллион квадратов и только потом first начал бы искать среди них подходящий. С lazy вычисление квадрата и проверка условия идут парой для каждого числа по очереди, и как только находится подходящее значение, дальше считать никто не станет.
Важна честная оговорка: выигрыш от lazy заметен только на больших коллекциях или на тяжёлых вычислениях внутри map. На маленьком массиве из десяти элементов разница в скорости никакая, а сам код с lazy читается чуть тяжелее и возвращает не обычный массив, а специальный ленивый тип, который иногда приходится дополнительно оборачивать в Array(...). Используйте lazy осознанно, а не по умолчанию — не ради моды.
Свой тип, который можно перебирать
Раз Sequence — это просто «умеет отдавать next()», ничего не мешает написать свой собственный тип с этим умением. Подробно про создание своих типов будет отдельный большой разговор дальше — здесь важна не структура сама по себе, а идея, что протокол можно реализовать вручную.
struct Countdown: Sequence, IteratorProtocol {
var current: Int
mutating func next() -> Int? {
guard current > 0 else { return nil }
let value = current
current -= 1
return value
}
}
for tick in Countdown(current: 3) {
print(tick)
}
// напечатает 3, потом 2, потом 1
Countdown хранит одно число — сколько осталось. Каждый вызов next() проверяет через guard, не дошли ли до нуля: если дошли, возвращает nil, и перебор останавливается. Если нет — запоминает текущее значение, уменьшает счётчик на единицу и отдаёт запомненное значение. Слово mutating здесь означает, что метод разрешено менять собственное свойство типа — про это будет отдельный подробный разговор, пока достаточно знать, что без него метод не смог бы изменить current.
Раз тип соответствует Sequence, для него сразу же становятся доступны map, filter, contains и остальные методы — писать их отдельно для Countdown не нужно. Это и есть главный практический вывод всего урока: реализовали один маленький протокол — получили десятки готовых возможностей бесплатно.
Частые ошибки
- Ждать, что цикл
forсработает на произвольном типе просто потому, что у него есть данные внутри. Нужно явное соответствиеSequenceс реализованнымnext(), само по себе оно не появляется. - Вызывать
next()у итератора и не проверять результат наnil— если продолжать вызыватьnext()после конца последовательности принудительным разворачиванием, программа упадёт. - Добавлять
lazyна маленькие коллекции и удивляться, что код стал длиннее, а не быстрее — на десятке элементов разница в производительности не измерима. - Забывать
mutatingу методаnext(), если он меняет собственное свойство типа — без этого слова компилятор откажется собирать код. - Путать
SequenceиCollection: если типу нужен доступ по индексу или мгновенныйcount, одногоSequenceнедостаточно, нуженCollection.
Резюме
Sequence— протокол «можно перебрать один за другим»; из него приходятmap,filter,reduce,contains,sortedи другие методы, написанные один раз для всех подходящих типов.CollectionрасширяетSequenceи добавляет многократный обход, доступ по индексу и мгновенныеcount,isEmpty,first.- Перебор через
for— это скрытый вызовmakeIterator()и повторные вызовыnext(), который возвращает опционал и сигналит об окончании значениемnil. lazyоткладывает вычисления до момента, когда значение реально понадобилось; выигрыш заметен только на больших данных или тяжёлых операциях.- Свой тип может соответствовать
SequenceиIteratorProtocol, реализовав всего один методnext(), и сразу получить весь набор стандартных методов коллекций.
Проверьте себя
3 вопроса
Свой перебираемый тип: чётные числа
В уроке был разобран тип Countdown, который считал вниз, реализуя Sequence и IteratorProtocol. Сделайте похожий тип для чётных чисел и функцию, которая берёт из него первые несколько значений.
Допишите структуру EvenSequence: у неё есть свойство current (текущее число, с которого начинается отсчёт), а метод next() должен КАЖДЫЙ раз возвращать текущее значение current и затем увеличивать current на 2 (эта последовательность бесконечная и никогда не возвращает nil сама по себе — её обязаны ограничивать снаружи).
Затем реализуйте функцию firstEvens(from start: Int, count n: Int) -> [Int], которая возвращает массив из первых n чётных чисел, начиная с ближайшего чётного числа, которое не меньше start (если start само чётное — начинайте прямо с него, если нечётное — со следующего за ним числа). Используйте цикл for по значению типа EvenSequence и останавливайте сбор, как только собрано нужное количество элементов. Если n равно нулю, верните пустой массив.
Примеры: firstEvens(from: 0, count: 5) вернёт [0, 2, 4, 6, 8]; firstEvens(from: 1, count: 3) вернёт [2, 4, 6], потому что 1 нечётное и отсчёт начинается с 2; firstEvens(from: -5, count: 4) вернёт [-4, -2, 0, 2].