OOP Course
Сьогодні

Підрозділ 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<T> та IEnumerator<T> — як працює foreach

Інтерфейс 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> для економії пам'яті.

Розроблено Tomka Yurii · © 2026 ·