Перейти к основному содержимому

Паттерн "Итератор" в C# — IEnumerator и yield return

Разработчику Архитектору

Обзор паттерна в контексте GoF — в поведенческих паттернах. Механика yield, yield break и обход деревьев — в главе про итераторы. Здесь — связка Iterator как идея и то, что в C# за ручную "машину состояний" часто отвечает компилятор.

Загрузка редактора схем…

Задача паттерна

Итератор даёт последовательный доступ к элементам составного объекта без раскрытия внутреннего представления (массив, связный список, дерево). Клиент вызывает один и тот же контракт обхода; детали хранения остаются внутри коллекции.

В .NET этот контракт — IEnumerable<T> и IEnumerator<T>:

ЧленРоль
IEnumerable<T>.GetEnumerator()Фабрика итератора по коллекции
IEnumerator<T>.MoveNext()Переход к следующему элементу
CurrentТекущий элемент
Reset()Сброс (в generic-итераторах почти не используется)
Dispose()Освобождение ресурсов

Цикл foreach разворачивается в вызовы GetEnumerator() и MoveNext().


Ручная реализация — как в учебнике

До появления итераторных методов в C# 2.0 для простого массива писали отдельный класс с позицией и ручным MoveNext():

public class NumberCollection : IEnumerable<int>
{
private readonly int[] _items = { 1, 2, 3, 4, 5 };

public IEnumerator<int> GetEnumerator()
=> new NumberEnumerator(_items);

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

public class NumberEnumerator : IEnumerator<int>
{
private readonly int[] _items;
private int _position = -1;

public NumberEnumerator(int[] items) => _items = items;

public int Current => _items[_position];
object IEnumerator.Current => Current;

public bool MoveNext()
{
_position++;
return _position < _items.Length;
}

public void Reset() => _position = -1;
public void Dispose() { }
}

Отдельный тип, индекс, IDisposable, явная нелексическая реализация IEnumerable — десятки строк только для линейного перебора. Паттерн соблюдён, но для повседневных задач такой объём редко оправдан.


yield return — итератор, который пишет компилятор

Начиная с C# 2.0 (2005) итераторный метод с yield return компилятор превращает во вложенный класс с MoveNext(), Current и сохранением локальных переменных и точки остановки.

Тот же смысл — несколько строк:

public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
}

При вызове GetNumbers() массив не создаётся. Возвращается объект-итератор; каждый шаг foreach или LINQ запрашивает следующий элемент.


Ленивость и бесконечные последовательности

Элемент вычисляется в момент запроса. Это важно для больших потоков и генераторов без верхней границы:

public static IEnumerable<int> EvenNumbers()
{
int n = 0;
while (true)
{
yield return n;
n += 2;
}
}

// первые 10 чётных чисел
var result = EvenNumbers().Take(10).ToList();

Take(10) обрывает цепочку до бесконечного цикла. Бесконечную последовательность через ручной IEnumerator тоже можно построить, но код читается тяжелее: состояние, граничные случаи и сброс позиции лежат на авторе.

Подробнее про yield break, обход деревьев и ограничения — в итераторах и yield.


Связка с LINQ

yield return и LINQ опираются на одну модель — отложенное перечисление (IEnumerable<T>).

Фильтрация и проекция итератором:

public static IEnumerable<string> ProcessUsers(IEnumerable<User> users)
{
foreach (var user in users)
{
if (user.IsActive)
yield return user.Name.ToUpper();
}
}

Эквивалент через LINQ:

var names = users
.Where(u => u.IsActive)
.Select(u => u.Name.ToUpper());

Оба варианта ленивые: промежуточный список появится только после терминальной операции (ToList(), ToArray(), foreach, Count() с полным проходом и т.п.). См. LINQ.

В BCL методы вроде Where и Select внутри реализованы через yield return — тот же приём, что вы используете в своём коде.


Когда ручной IEnumerator<T> всё же уместен

yield return закрывает большинство сценариев обхода. Ручная реализация или явный класс итератора оправданы, когда:

СитуацияПочему не только yield
Асинхронный потокIAsyncEnumerable<T> / IAsyncEnumerator<T>, отмена через CancellationToken
Сложный DisposeНужно гарантированно закрыть файл, соединение или снять блокировку при досрочном выходе из foreach
Нетривиальный обходГраф с меткой посещённых вершин, обход с возвратом, несколько независимых курсоров с общим состоянием
Интеграция с нативным APIВнешний курсор, который нельзя выразить итераторным методом

Для асинхронных последовательностей в современном C# есть async-итераторы (async IAsyncEnumerable<T> с yield return внутри async-метода) — отдельная тема в справочнике C#.


Чек-лист

ЗадачаРекомендация
Линейный или древовидный обход, генераторyield return
Цепочка фильтров и проекцийLINQ или свой метод с yield
Нужен материализованный снимокToList() / ToArray() в конце
Асинхронность, тонкий lifecycle ресурсовIAsyncEnumerator<T> или ручной итератор
Простой List<T> / массивВстроенный foreach, свой итератор не нужен
Паттерн и язык

В C# Iterator как паттерн GoF встроен в платформу: foreach, IEnumerable<T> и генерация итератора компилятором. Ручной класс с MoveNext() имеет смысл учить и писать, когда вы понимаете, что делает yield под капотом — или когда сценарий выходит за рамки итераторного метода.


Итог

Паттерн Итератор в .NET — это единый способ обхода без привязки к конкретной структуре данных. Ручной IEnumerator<T> — полный контроль и учебная модель. yield return поручает машину состояний компилятору и остаётся стандартным выбором для ленивых последовательностей и связки с LINQ.

Перед написанием отдельного класса *Enumerator для простого перебора стоит проверить, хватит ли итераторного метода из нескольких строк.


См. также

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").