Перейти к содержанию

Iterator (Итератор)

Категория: паттерн поведения.

Проблема

Коллекции внутри устроены по-разному: массив, связный список, дерево, кольцевой буфер, набор данных из внешнего источника. Если клиентский код обходит коллекцию, опираясь на её внутреннее устройство (например, обращаясь по индексу), он оказывается жёстко привязан к этому устройству - смена структуры хранения ломает весь код обхода, а сам код обхода невозможно переиспользовать для коллекции другого вида.

Решение

  1. Вводится объект-итератор, который умеет последовательно возвращать элементы коллекции через единый интерфейс (MoveNext/Current в .NET).
  2. Коллекция предоставляет метод для получения такого итератора, но прячет от клиента, как именно устроено хранение элементов внутри.
  3. Клиентский код работает с итератором единообразно, независимо от реальной структуры данных за ним.

В 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 - асинхронный вариант того же паттерна для потоковых/асинхронных источников данных.

Пример реализации на C#

Iterator.cs
using System;
using System.Collections;
using System.Collections.Generic;

namespace DesignPatterns.Behavioral.Iterator
{
    // Собственная коллекция со скрытой внутренней структурой (кольцевой буфер).
    // Реализуя IEnumerable<T>, мы даём клиенту единый способ обхода (foreach),
    // не раскрывая, что внутри на самом деле массив фиксированного размера с "головой" и "хвостом".
    public sealed class RingBuffer<T> : IEnumerable<T>
    {
        private readonly T[] _items;
        private int _head;
        private int _count;

        public RingBuffer(int capacity)
        {
            _items = new T[capacity];
        }

        public void Add(T item)
        {
            int index = (_head + _count) % _items.Length;
            _items[index] = item;

            if (_count < _items.Length)
            {
                _count++;
            }
            else
            {
                _head = (_head + 1) % _items.Length; // самый старый элемент затирается
            }
        }

        // Благодаря yield return компилятор сам генерирует класс-итератор,
        // реализующий IEnumerator<T> - вручную его писать не нужно.
        public IEnumerator<T> GetEnumerator()
        {
            for (int i = 0; i < _count; i++)
            {
                int index = (_head + i) % _items.Length;
                yield return _items[index];
            }
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

    public static class Demo
    {
        public static void Run()
        {
            var buffer = new RingBuffer<int>(capacity: 3);
            buffer.Add(1);
            buffer.Add(2);
            buffer.Add(3);
            buffer.Add(4); // 1 будет вытеснен

            // Клиентский код использует обычный foreach и ничего не знает
            // про кольцевой буфер, "голову" и "хвост" внутри.
            foreach (int item in buffer)
            {
                Console.WriteLine(item);
            }
        }
    }
}

Открыть Iterator.cs отдельно Скачать Iterator.cs