Что обязан делать итератор Sequence при вызове next() после того, как уже вернул nil?
После первого возврата nil итератор считается исчерпанным и при последующих вызовах next() должен снова возвращать nil. Он не должен возобновлять выдачу элементов: nil означает окончательный конец именно этого обхода.
Протоколы Sequence и IteratorProtocol отделяют способ обхода данных от их хранения. Благодаря этому один и тот же алгоритм может работать с массивом, множеством, ленивой последовательностью или пользовательским источником элементов.
Чтобы потребитель мог универсально завершать цикл, протокол использует Optional: значение элемента означает продолжение обхода, а nil — его окончание. Контракт исчерпания нужен для предсказуемой работы стандартных алгоритмов обхода.
Если итератор после nil снова начнёт возвращать элементы, алгоритм, который остановился на первом nil, уже не сможет корректно продолжить обход. Это приводит к пропущенным данным, рассинхронизации состояния и потенциально бесконечным или непредсказуемым циклам.
Особенно опасна ошибка, при которой nil трактуется как временное отсутствие элемента. Для синхронного IteratorProtocol это не пауза и не сигнал «попробовать позже», а окончательное завершение итератора.
Метод next() хранит состояние текущего положения итератора. Пока элементы доступны, он возвращает очередной элемент и продвигает состояние; когда источник исчерпан, возвращает nil и переводит итератор в состояние завершения.
После этого состояние должно оставаться исчерпанным. Стандартные операции последовательностей могут вызвать next() повторно, поэтому корректная реализация обязана стабильно возвращать nil, а не рассчитывать на то, что потребитель обратится к итератору только один раз.
В этом примере после выдачи двух элементов итератор возвращает nil и сохраняет это состояние. Сам IteratorProtocol не требует конкретного способа хранения состояния, поэтому оно может быть индексом, ссылкой на внешний ресурс или более сложной структурой.
Важно отличать итератор от последовательности. Вызов makeIterator() создаёт объект обхода, а состояние продвижения принадлежит именно итератору. Если источник поддерживает повторный обход, это достигается созданием нового итератора, а не восстановлением уже исчерпанного.
Разработчик реализует последовательность для чтения страниц из локального файла. Он возвращает nil, когда текущая страница временно не загружена, рассчитывая получить её при следующем вызове next(). Такой подход нарушает контракт: потребитель завершит обход, приняв nil за конец данных.
Можно сохранить этот подход и нарушить ожидаемое поведение, но тогда стандартные алгоритмы Sequence становятся ненадёжными. Можно вручную добавить отдельный статус «временно недоступно», однако это уже будет собственный API, несовместимый с обычным next().
Корректное решение — возвращать nil только после окончательного достижения конца файла, а ожидание, повторные попытки и ошибки вынести в отдельный интерфейс. Для асинхронного источника уместнее использовать модель, поддерживающую ожидание и ошибки, например AsyncThrowingSequence. В результате обычный синхронный обход получает строгую семантику завершения, а временные состояния не маскируются под конец данных.
Нет. Первый nil фиксирует состояние исчерпания, а каждый последующий вызов next() также должен возвращать nil. Требование относится не к количеству вызовов, а к тому, что после исчерпания новые элементы появляться не должны.
Нет, результат будет одинаковым: первый вызов next() вернёт nil. Это нормально, потому что IteratorProtocol описывает текущий обход, а не историю того, были ли элементы раньше. Если нужно различать эти состояния, информацию следует хранить отдельно на уровне источника или собственного протокола.
Нет, не тот же самый итератор. Для нового обхода следует получить новый итератор через makeIterator(), если конкретная Sequence поддерживает такую операцию. Сброс состояния внутри уже исчерпанного итератора нарушил бы контракт IteratorProtocol и сделает поведение стандартных алгоритмов непредсказуемым.