Підрозділ 10.7
Інтерфейси IEnumerable<T> та IEnumerator<T>
10.7. Інтерфейси IEnumerable<T та IEnumerator<T Щоразу, коли ви пишете foreach var p in ward , C виконує певний контракт — звертається до колекції через стандартизовані інтерфейси. Цей контракт описується двома
10.7. Інтерфейси IEnumerable та IEnumerator
Щоразу, коли ви пишете foreach (var p in ward), C# виконує певний контракт — звертається до колекції через стандартизовані інтерфейси. Цей контракт описується двома інтерфейсами: IEnumerable<T> і IEnumerator<T>.
Розуміння цих інтерфейсів пояснює, чому foreach однаково працює з масивом, List<T>, Queue<T>, Dictionary<K,V>, власним класом пацієнтів або лінивим генератором. Усі вони реалізують один і той самий контракт.
Що компілятор робить з foreach
Конструкція foreach — синтаксичний цукор. Компілятор розгортає її у явний виклик методів перелічувача:
// Що ви пишете:
foreach (var p in ward)
Console.WriteLine(p);
// Що компілятор генерує:
var e = ward.GetEnumerator();
try
{
while (e.MoveNext())
{
var p = e.Current;
Console.WriteLine(p);
}
}
finally
{
e.Dispose();
}Ці методи GetEnumerator(), MoveNext(), Current, Dispose() — і є контрактом двох інтерфейсів.

Інтерфейс IEnumerable
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}IEnumerable<T> відповідає на одне питання: «дай мені перелічувач». Саме цей інтерфейс перевіряє компілятор, коли ви пишете foreach. Якщо об'єкт реалізує IEnumerable<T> — він допускається до foreach.
Реалізують: List<T>, T[], Queue<T>, Stack<T>, Dictionary<K,V>, ObservableCollection<T>, LinkedList<T> та будь-який власний клас, якому ви додасте цей інтерфейс.
Інтерфейс IEnumerator
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
bool MoveNext(); // перейти до наступного елемента
T Current { get; } // повернути поточний елемент
void Reset(); // повернутись на початок (рідко використовується)
void Dispose(); // звільнити ресурси
}IEnumerator<T> — сам перелічувач. Він зберігає поточну позицію всередині колекції. Кожен виклик MoveNext() зсуває позицію на один крок і повертає true, поки є елементи; коли елементи вичерпані — повертає false і цикл while завершується.
Власний IEnumerable — runnable приклад
Щоб власний клас підтримував foreach, достатньо реалізувати IEnumerable<T>. Нижче — клас Ward (відділення), який перебирає тільки критичних пацієнтів:
Зверніть увагу: всередині GetEnumerator() використовується yield return (детально — розділ 10.8). Це найпростіший спосіб реалізувати власний перелічувач без написання окремого класу.
Явна реалізація IEnumerator — runnable приклад
Для повного розуміння механізму — клас RangeEnumerator, який перебирає числа від from до to без зберігання їх у пам'яті:
Цей приклад показує, що IEnumerator<T> — звичайний клас зі станом (_current). MoveNext() зсуває стан; Current повертає поточне значення. foreach просто викликає їх у потрібному порядку.
Коли реалізувати IEnumerable?
| Сценарій | Рішення |
|---|---|
| Власний клас-колекція | Реалізуйте IEnumerable<T> |
| Метод повертає послідовність | Використовуйте yield return (10.8) |
| Тільки читати чужу колекцію | Оголошуйте параметр як IEnumerable<T> |
Потрібні індексація та Count |
Використовуйте IList<T> або List<T> |
Параметр методу типу IEnumerable<T> замість List<T> — хороша практика: метод прийматиме масив, List<T>, Queue<T> та будь-яку іншу послідовність без змін.
Відкладене виконання (deferred execution)
Одна з найважливіших і найбільш неочікуваних особливостей IEnumerable<T> — відкладене виконання. Метод із yield return (або LINQ-запит, що повертає IEnumerable<T>) не виконується в момент виклику — він виконується лише тоді, коли результат фактично перебирається (у foreach, при виклику .ToList(), .First() тощо).
Генератор запускається тільки при першому MoveNext(). Після кожного yield return виконання призупиняється і повернеться до генератора лише при наступному MoveNext().
Багаторазове виконання — «подвійна пастка»
Оскільки кожен foreach викликає GetEnumerator() знову, генератор виконується з нуля при кожному переборі:
Правило: якщо IEnumerable<T> потребує дорогої операції (запит до БД, HTTP-запит, читання файлу) — матеріалізуйте результат через .ToList() або .ToArray() перед повторним використанням. Якщо послідовність велика і потрібна лише для одного проходу — залиште IEnumerable<T> для економії пам'яті.