JavaRush /Курсы /Swift SELF /Итераторы и for-in: что такое IteratorProtocol

Итераторы и for-in: что такое IteratorProtocol

Swift SELF
18 уровень , 1 лекция
Открыта

1. Как for-in получает элементы

Если вы только начинаете программировать, for item in items { ... } выглядит как честная магия: где-то внутри Swift “сам” достаёт элементы, “сам” понимает, когда остановиться, и ещё и делает это одинаково для массива, строки, словаря и диапазона. В этой лекции мы аккуратно снимем шляпу с кролика: увидим, что for-in работает через итератор, и это не страшно — это просто “устройство для выдачи следующего элемента”.

Представьте диспенсер с жвачками: нажимаешь кнопку — выпадает следующая жвачка. Когда жвачки кончились — диспенсер честно говорит “всё”. Итератор — примерно такой диспенсер, только для элементов.

Ментальная модель for-in

Когда мы пишем for x in numbers { ... }, Swift не “угадывает” элементы. Он берёт объект, который можно перебирать (то есть Sequence), запрашивает у него итератор и дальше в цикле снова и снова вызывает у итератора next().

next() либо возвращает очередной элемент, либо возвращает nil, что означает “элементы закончились”. Такой дизайн не случайный: nil здесь — максимально естественный сигнал окончания, и он отлично ложится на нашу прошлую тему про Optional.

Схематично это можно представить так:

flowchart TD
    A["Sequence (например Array)"] -->|"makeIterator()"| B["Iterator"]
    B -->|"next() -> Element?"| C{"Есть элемент?"}
    C -->|да| D["Тело цикла for-in"]
    D --> B
    C -->|"нет (nil)"| E["Выход из цикла"]

Обратите внимание на важную мелочь: for-in работает с Sequence, а не с итератором напрямую. Итератор — это “механизм выдачи”, а Sequence — “фабрика итераторов”.

IteratorProtocol: контракт итератора

Ключевое слово дня: IteratorProtocol. Это протокол (контракт), который описывает, что такое итератор в Swift.

На уровне идеи всё очень коротко: итератор обязан уметь возвращать “следующий элемент” или nil, если элементы закончились. В стандартной библиотеке это выражено методом next() -> Element?.

Важно, что next() обычно меняет внутреннее состояние итератора (он продвигается вперёд), поэтому next() часто является mutating.

А как итератор появляется? У последовательности есть метод makeIterator(), который создаёт новый итератор.

Если хочется одной фразой:

  • Sequence говорит: “я умею создать итератор”
  • IteratorProtocol говорит: “я умею выдать следующий элемент”

Технически это связано и через ассоциированные типы: у Sequence есть Iterator, а у итератора есть Element.

3. Делаем for-in вручную: makeIterator() и next()

Чтобы перестать верить в магию, полезно один раз сделать то же самое вручную. Возьмём массив и пройдёмся по нему через итератор.

import Foundation

let numbers = [10, 20, 30]

var it = numbers.makeIterator()
print(it.next() as Any)  // Optional(10)
print(it.next() as Any)  // Optional(20)
print(it.next() as Any)  // Optional(30)
print(it.next() as Any)  // nil

Почему тут as Any? Потому что print не любит печатать “голый” Optional в некоторых контекстах так, как вам хочется, а Any — универсальный “контейнер для вывода”. Главное: четвёртый next() возвращает nil, и это и есть честный сигнал “всё, конец”.

Теперь сделаем это в более “цикловом” стиле, и заодно потренируем while let, который мы уже знаем из темы про Optional:

import Foundation

let numbers = [10, 20, 30]

var it = numbers.makeIterator()
while let value = it.next() {
    print("value =", value)
}
// value = 10
// value = 20
// value = 30

Этот код читается почти по-русски: “пока next() возвращает значение — печатай его”.

И это очень близко к тому, что делает for-in, только for-in выглядит приятнее и короче. Можно сказать, что for-in — это “официальный красивый фасад”, а итератор — “механизм за фасадом”.

4. Состояние итератора: почему нужен var и как легко пропустить элемент

Когда вы впервые видите var it = ..., хочется спросить: “Почему var? Почему нельзя let? Мы же не меняем итератор, мы просто читаем элементы!” И вот тут Swift мягко, но настойчиво объясняет: при каждом next() итератор сдвигается вперёд. Значит, состояние меняется. А раз состояние меняется — нужен var.

Давайте покажем это через очень частую “новичковую” ошибку. Вот так писать нельзя:

import Foundation

let numbers = [1, 2, 3]

let it = numbers.makeIterator()
it.next() // ❌ Ошибка компиляции: нельзя мутировать 'let'

Swift не вредничает. Он буквально защищает вас от мысли “итератор неизменный”, потому что он изменяемый по своей природе.

Теперь про “расходуется”. Итератор — одноразовая штука: вы не можете “отмотать назад”, потому что он хранит только текущее положение. Если вы сделали несколько next(), то первые элементы уже “прошли”.

Покажем на примере, где легко случайно пропустить элементы:

import Foundation

let words = ["one", "two", "three"]

var it = words.makeIterator()
print(it.next() ?? "nil")  // one
print(it.next() ?? "nil")  // two

let skipped = it.next()
let alsoSkipped = it.next()

print(skipped ?? "nil")     // three
print(alsoSkipped ?? "nil") // nil

Если вы где-то случайно вызвали next() “просто проверить”, вы реально продвинулись вперёд и потеряли элемент для дальнейшей логики. Это не баг Swift — это нормальная цена за простую модель “дай следующий”.

5. Одноразовость: итератор vs последовательность

Теперь аккуратно разделим две похожие, но разные вещи: итератор и последовательность.

Итератор одноразовый почти всегда: он продвигается и заканчивается.

А вот последовательность (Sequence) может быть “повторяемой” или “одноразовой” — в зависимости от того, как она устроена. Массив можно перебирать хоть десять раз: каждый раз создаётся новый итератор, и всё начинается сначала.

import Foundation

let numbers = [1, 2, 3]

for x in numbers {
    print("first pass:", x)
}

for x in numbers {
    print("second pass:", x)
}
// first pass: 1
// first pass: 2
// first pass: 3
// second pass: 1
// second pass: 2
// second pass: 3

Почему так? Потому что numbers.makeIterator() каждый раз создаёт новый итератор, а итератор стартует с начала.

Но важная идея такая: Sequence по контракту обещает “можно перебирать”, но не всегда обещает “можно перебирать много раз одинаково”. На нашем уровне это скорее предупреждение для здравого смысла: если источник данных “живой” (например, поток данных или генератор), то повторный проход может быть невозможен или даст другие результаты.

Сегодня мы не будем строить свои генераторы и не будем лезть в тонкости “одноразовых” последовательностей, но модель “итератор расходуется” вам нужна прямо сейчас — она объясняет поведение next() и необходимость Optional.

6. Практика: “каталог книг” и ранняя остановка

Чтобы тема не осталась абстрактной, давайте продолжим линию учебного приложения в стиле “консольный каталог”. Пусть у нас есть список книг (обычный [String]), и мы хотим вывести первые N книг, которые подходят под условие (например, содержат подстроку). Это типичный кейс, где полезно понимать, что элементы идут по одному и можно остановиться раньше.

Сначала сделаем версию на for-in (она самая читаемая, и в реальной жизни вы будете писать её чаще всего):

import Foundation

let books = ["Swift Basics", "Algorithms in Swift", "Kotlin Intro", "Swift Regex"]

let needle = "Swift"
var printed = 0

for title in books {
    if title.contains(needle) {
        print(title)
        printed += 1
        if printed == 2 { break }
    }
}
// Swift Basics
// Algorithms in Swift

Здесь break — наш “стоп-кран”: как только нашли 2 книги, заканчиваем.

А теперь сделаем примерно то же самое, но через итератор, чтобы почувствовать механику “в руках”. С точки зрения алгоритма ничего не меняется: мы всё так же читаем элементы до конца или до раннего выхода.

import Foundation

let books = ["Swift Basics", "Algorithms in Swift", "Kotlin Intro", "Swift Regex"]

let needle = "Swift"
var printed = 0

var it = books.makeIterator()
while let title = it.next() {
    if title.contains(needle) {
        print(title)
        printed += 1
        if printed == 2 { break }
    }
}
// Swift Basics
// Algorithms in Swift

Обе версии корректны, и обе остановятся раньше конца массива. Разница в том, что for-in красивее, а ручной итератор полезен как “рентген”: он помогает понять, что внутри всё равно идёт чтение по одному элементу через next().

И вот это понимание пригодится вам буквально через одну-две темы курса, где появятся “цепочки преобразований” данных: вы начнёте гораздо лучше чувствовать, когда данные реально обходятся, где возможна ранняя остановка, и почему иногда неожиданный print “внутри преобразования” срабатывает не тогда, когда вы ожидали.

7. Типичные ошибки при работе с итераторами

Ошибка №1: объявить итератор через let и удивляться, что next() “не работает”.
На старте кажется, что итератор — это “ссылка на массив”, и его можно сделать константой. Но next() меняет внутреннее состояние, поэтому итератор почти всегда должен быть var. Если компилятор ругается на “mutating member on immutable value”, он не вредничает — он показывает вам, что итератор продвигается.

Ошибка №2: вызвать next() два раза в одном логическом шаге и случайно пропустить элемент.
Очень распространённая история: сначала вы делаете if it.next() != nil { ... }, а потом внутри снова вызываете it.next() “чтобы взять значение”. В итоге вы съели два элемента, хотя хотели один. Правило простое: если вызвали next(), сохраните результат в переменную и работайте с ней.

Ошибка №3: относиться к nil как к “ошибке”, а не как к “концу”.
В теме Optional мы привыкали, что nil часто означает отсутствие данных, и это может быть ошибкой ввода или отсутствием ключа в словаре. У итератора nil — это нормальная, штатная ситуация: “элементы закончились”. Если вы начинаете логировать это как ошибку или пытаться “починить”, вы воюете с дизайном языка.

Ошибка №4: путать итератор и последовательность и пытаться “перебирать итератор повторно”.
Если вы руками достали итератор и прошли его до конца, то второй раз тем же итератором вы уже ничего не получите. Чтобы пройти заново, нужен новый итератор (то есть снова makeIterator()). Это особенно важно, когда вы временно отлаживаете код и делаете print(it.next()) “просто посмотреть”: вы реально меняете состояние и можете сломать дальнейшую логику.

Ошибка №5: пытаться строить логику на предположении о порядке там, где порядок не обещан.
Итератор выдаёт элементы в порядке, который определён конкретным типом. У массива порядок очевидный, а у Set или Dictionary порядок перебора не должен быть основой вашей логики. Иначе вы получите программу, которая “вроде работает”, пока не перестанет — и это будет самый неприятный вид багов: недетерминированный.

1
Задача
Swift SELF, 18 уровень, 1 лекция
Недоступна
Итератор склада
Итератор склада
1
Задача
Swift SELF, 18 уровень, 1 лекция
Недоступна
Лента слов
Лента слов
1
Задача
Swift SELF, 18 уровень, 1 лекция
Недоступна
Поиск делимости
Поиск делимости
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ