Паттерн "Итератор" в C# — IEnumerator и yield return
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Обзор паттерна в контексте GoF — в поведенческих паттернах. Механика yield, yield break и обход деревьев — в главе про итераторы. Здесь — связка Iterator как идея и то, что в C# за ручную "машину состояний" часто отвечает компилятор.
Play ITЗагрузка интерактивного демо…
Задача паттерна
Итератор даёт последовательный доступ к элементам составного объекта без раскрытия внутреннего представления (массив, связный список, дерево). Клиент вызывает один и тот же контракт обхода; детали хранения остаются внутри коллекции.
В .NET этот контракт — IEnumerable<T> и IEnumerator<T>:
| Член | Роль |
|---|---|
IEnumerable<T>.GetEnumerator() | Фабрика итератора по коллекции |
IEnumerator<T>.MoveNext() | Переход к следующему элементу |
Current | Текущий элемент |
Reset() | Сброс (в generic-итераторах почти не используется) |
Dispose() | Освобождение ресурсов |
Цикл foreach разворачивается в вызовы GetEnumerator() и MoveNext().
Ручная реализация — как в учебнике
До появления итераторных методов в C# 2.0 для простого массива писали отдельный класс с позицией и ручным MoveNext():
Код ITЗагрузка примера кода…
Отдельный тип, индекс, 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 запрашивает следующий элемент.
Компилятор генерирует класс с полем _state и MoveNext() в виде конечного автомата; foreach вызывает MoveNext() на каждой итерации, локальные переменные метода становятся полями этого класса. Псевдокод трансформации, поток из IDataReader, аллокации и параллель с async/await — в главе про yield, раздел "Под капотом".
Ленивость и бесконечные последовательности
Элемент вычисляется в момент запроса. Это важно для больших потоков и генераторов без верхней границы:
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 для простого перебора стоит проверить, хватит ли итераторного метода из нескольких строк.
См. также
- Поведенческие паттерны — Итератор
- Итератор в Java
- Итераторы и yield
- LINQ
- Паттерн "Стратегия" в C# — другой поведенческий паттерн с делегатами