Iterator (Итератор)¶
Категория: паттерн поведения.
Проблема¶
Коллекции внутри устроены по-разному: массив, связный список, дерево, кольцевой буфер, набор данных из внешнего источника. Если клиентский код обходит коллекцию, опираясь на её внутреннее устройство (например, обращаясь по индексу), он оказывается жёстко привязан к этому устройству - смена структуры хранения ломает весь код обхода, а сам код обхода невозможно переиспользовать для коллекции другого вида.
Решение¶
- Вводится объект-итератор, который умеет последовательно возвращать элементы коллекции через единый интерфейс (
MoveNext/Currentв .NET). - Коллекция предоставляет метод для получения такого итератора, но прячет от клиента, как именно устроено хранение элементов внутри.
- Клиентский код работает с итератором единообразно, независимо от реальной структуры данных за ним.
В C# эта идея встроена прямо в язык через интерфейсы IEnumerable<T>/IEnumerator<T> и оператор foreach, а также через синтаксис yield return, который избавляет от необходимости писать класс-итератор вручную.
Структура¶
Iterator(IEnumerator<T>) - интерфейс с методами перехода к следующему элементу и получения текущего.ConcreteIterator- хранит состояние обхода конкретной коллекции.Aggregate(IEnumerable<T>) - интерфейс коллекции, предоставляющей итератор.ConcreteAggregate- конкретная коллекция с собственной внутренней структурой.
Особенности итераторов в C#/.NET¶
yield return- компилятор автоматически генерирует класс, реализующийIEnumerator<T>, из метода сyield return, что избавляет от ручного написания состояния обхода.- Ленивость - последовательность, построенная через
yield return, вычисляется лениво: элемент не создаётся, пока к нему реально не обратились черезMoveNext/Current. На этом построен весь LINQ to Objects (Where,Selectи т.д. не выполняются, пока не начнётся перечисление). - Валидность итератора - большинство коллекций BCL бросают
InvalidOperationException, если коллекция изменяется во время активногоforeachпо ней (эта проверка защищает от рассинхронизации состояния итератора с изменившейся коллекцией). foreachнад структурами - если тип реализуетGetEnumerator()как метод, возвращающийstruct, реализующий нужный контракт (даже без формальной реализации интерфейсаIEnumerable<T>- "duck typing" дляforeach), можно избежать накладных расходов на упаковку (boxing) и виртуальные вызовы - именно так устроены перечислители уList<T>иDictionary<TKey, TValue>.
Когда применять¶
- Клиенту нужно перебрать элементы коллекции, не зная и не завися от её внутреннего устройства.
- Один и тот же обходящий код должен уметь работать с разными видами коллекций.
- Нужна ленивая последовательность, элементы которой вычисляются по требованию, а не создаются все сразу (полезно для потенциально бесконечных или очень больших последовательностей).
Плюсы¶
- Изолирует код обхода от внутренней структуры коллекции.
- Позволяет иметь несколько независимых активных обходов одной и той же коллекции одновременно.
- В сочетании с
yield returnдаёт ленивые вычисления почти бесплатно, без ручного написания состояния итератора.
Минусы¶
- Для очень простых, разово используемых коллекций (например, обход одного массива внутри метода) абстракция итератора избыточна - хватает обычного
for. - Итератор, построенный над изменяемой коллекцией, требует аккуратности: изменение коллекции во время обхода - частый источник ошибок в рантайме.
Пример в .NET Framework / BCL¶
IEnumerable<T>/IEnumerator<T>и операторforeach- реализация паттерна Iterator прямо в основе языка.- Весь LINQ to Objects (
Where,Select,OrderBy, ...) - цепочки итераторов, ленивая последовательность операций надIEnumerable<T>. IAsyncEnumerable<T>иawait foreach- асинхронный вариант того же паттерна для потоковых/асинхронных источников данных.