OOP Course
Сьогодні

Підрозділ 10.8

Ітератори та yield return

10.8. Ітератори та yield return У розділі 10.7 ми побачили, що для реалізації IEnumerable<T необхідно або написати окремий клас перелічувач IEnumerator<T , або скористатися yield return у тілі GetEnumerator . Д

10.8. Ітератори та yield return

У розділі 10.7 ми побачили, що для реалізації IEnumerable<T> необхідно або написати окремий клас-перелічувач (IEnumerator<T>), або скористатися yield return у тілі GetEnumerator(). Другий варіант — набагато простіший і значно поширеніший на практиці.

yield return — це ключове слово, яке перетворює звичайний метод на ітераторний метод. Компілятор сам генерує клас-перелічувач з усіма необхідними полями та методами. Ви описуєте логіку обходу; C# будує машину стану.

Синтаксис і базовий принцип

Метод вважається ітераторним, якщо:

  1. Тип повернення — IEnumerable<T>, IEnumerator<T> або їхні негенерик-варіанти (а для асинхронних ітераторів з C# 8 — IAsyncEnumerable<T> чи IAsyncEnumerator<T> разом з модифікатором async, розділ 17.6).
  2. Тіло містить хоча б один yield return або yield break.
IEnumerable<int> Sequence()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

Кожен виклик yield return повертає один елемент і призупиняє виконання. При наступному виклику MoveNext() виконання відновлюється з тієї ж точки. yield break завершує ітерацію достроково. Якщо ж виконання просто дійшло до кінця тіла методу, ітерація теж завершується (MoveNext() повертає false) — писати yield break наприкінці не потрібно.

На ітераторні методи компілятор накладає кілька обмежень, що випливають з перетворення на машину стану: параметри не можуть бути ref, out чи in; yield return не можна писати в блоці try, який має catch (у try з finally — можна), і в самому catch/finally; у тілі не можна використовувати unsafe-код. Блоки finally в ітераторі виконуються, коли перебір завершився або коли споживач викликав Dispose() перелічувача (наприклад, після break у foreach).

Ліниве виконання

Ключова властивість ітераторних методів: тіло методу не виконується в момент виклику. Воно запускається лише тоді, коли foreach або інший споживач запросить перший елемент.

yield return — ліниве виконання ітератораyield return — ліниве виконання ітератора

Схема показує цей процес покроково: виклик методу лише створює об'єкт; перший MoveNext() виконує тіло до першого yield return; кожен наступний — продовжує з місця паузи (і пропускає неактивних пацієнтів), а коли метод доходить до кінця, MoveNext() повертає false.

Із лінивості є неочевидний наслідок: перевірки аргументів усередині ітератора теж відкладаються. Якщо метод на початку робить if (all == null) throw new ArgumentNullException(...), виняток вилетить не в рядку виклику, а лише на першому MoveNext() — можливо, далеко від місця помилки. Тому в бібліотечному коді перевірки виносять у звичайний (не ітераторний) метод-обгортку, який перевіряє аргументи й одразу повертає результат внутрішньої локальної функції-ітератора.

IEnumerable<string> GetActivePatients(List<Patient> all)
{
    Console.WriteLine("Початок перебору");  // виконається при першому MoveNext()
    foreach (var p in all)
        if (p.IsActive)
            yield return p.Name;            // пауза до наступного MoveNext()
    Console.WriteLine("Перебір завершено");
}

// Тут метод ще НЕ виконується:
var seq = GetActivePatients(patients);

// Тут починається фактичне виконання:
foreach (var name in seq)
    Console.WriteLine(name);

yield return у GetEnumerator — runnable приклад

Найпоширеніший патерн: реалізація IEnumerable<T> без окремого класу-перелічувача. Клас Department фільтрує пацієнтів за статусом прямо в GetEnumerator():

yield return як самостійний метод — runnable приклад

yield return може бути у будь-якому методі з типом повернення IEnumerable<T>, не лише в GetEnumerator(). Це дозволяє будувати ланцюжки фільтрів і генераторів:

Що компілятор генерує з yield return

yield return — це синтаксичний цукор. Компілятор перетворює ітераторний метод на клас-машину стану з полями для всіх локальних змінних та покажчиком поточного кроку:

Ваш код:
  IEnumerable<T> MyMethod() { yield return a; yield return b; }

Генерований код (спрощено):
  class MyMethod_StateMachine : IEnumerator<T>
  {
      int _state = 0;
      T   _current;

      bool MoveNext() {
          switch (_state) {
              case 0: _current = a; _state = 1; return true;
              case 1: _current = b; _state = 2; return true;
              default: return false;
          }
      }
      T Current => _current;
  }

Саме тому між двома yield return зберігається стан усіх локальних змінних: вони стають полями класу.

yield return vs явний IEnumerator

Критерій yield return Явний IEnumerator
Обсяг коду Мінімальний Значний (клас зі станом)
Читабельність Висока Середня
Повний контроль Частковий (Reset() кидає NotSupportedException) Повний (Reset, Dispose)
Ліниве виконання Автоматично Вручну
Коли використовувати 95% випадків Складні нестандартні сценарії

yield return та IEnumerable — підсумок розділу 10

Розділ 10 охоплює повний ланцюжок від конкретних колекцій до абстракцій:

List<T>               — індексована, динамічна
LinkedList<T>         — двозв'язний список, O(1) вставка
Queue<T>              — FIFO-черга
Stack<T>              — LIFO-стек
Dictionary<K,V>       — хеш-таблиця
ObservableCollection  — з подією CollectionChanged
IEnumerable<T>        — контракт «можна перебрати foreach»
yield return          — генерує IEnumerator<T> без зайвого коду

Кожен рівень дає додатковий рівень абстракції: foreach не знає, що за нею стоїть — список у пам'яті, черга у БД чи генератор з yield return. Вона просто викликає MoveNext().

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