Паттерн "Итератор" в 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 для простого перебора стоит проверить, хватит ли итераторного метода из нескольких строк.
См. также
- Поведенческие паттерны — Итератор
- Итераторы и yield
- LINQ
- Паттерн "Стратегия" в C# — другой поведенческий паттерн с делегатами
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Паттерн — это повторяющийся шаблон, узор или схема. Паттерны встречаются повсюду — в природе, архитектуре, поведении людей и, конечно, в программировании. Порождающие паттерны проектирования — это группа шаблонов, направленных на решение задач, связанных с созданием объектов. Структурные паттерны — это группа шаблонов проектирования, решающих задачи организации классов и объектов таким образом, чтобы обеспечить гибкую архитектуру программного обеспечения. Поведенческие паттерны — это группа шаблонов проектирования, которые определяют способы взаимодействия объектов и распределения ответственности между ними. Архитектурные паттерны — это проверенные решения для организации структуры программного обеспечения. Интеграция систем — одна из центральных задач в современной разработке программного обеспечения. Паттерны доменного моделирования представляют собой проверенные решения для организации бизнес-логики в программных системах. Паттерн Strategy в C# — классическая реализация через интерфейс, замена на Func и Action, DI и критерии выбора без лишних абстракций. Abstract Factory в C# и .NET — классическая схема через интерфейсы, замена через DI-контейнер, фабричный делегат и keyed services в .NET 8. Паттерн Command в C# — классическая схема, делегаты, MediatR, очередь задач, undo и критерии выбора между объектом команды и простым вызовом сервиса. Паттерн Observer в C# — event и делегаты, IObservable IObserver, слабая связанность, отписка и как не поймать утечки памяти в долгоживущих сервисах.Обзор паттернов проектирования
Порождающие паттерны
Структурные паттерны
Поведенческие паттерны
Архитектурные паттерны
Паттерны интеграции внешних систем
Паттерны проектирования доменных моделей
Стратегия в C#
Фабрика в C#
Команда в C#
Наблюдатель в C#