JavaRush /Курси /C# SELF /Контракт на перелічуваність:

Контракт на перелічуваність: IEnumerable<T>

C# SELF
Рівень 28 , Лекція 1
Відкрита

1. Вступ

Як ви, мабуть, уже помічали, різні колекції C# мають спільні риси. Наприклад, майже будь-яку з них можна обійти через цикл foreach:


var names = new List<string> { "Аня", "Борис", "Віка" };

// Увага — код працює і з List, і з масивом, і навіть з HashSet!
foreach (var name in names)
{
    Console.WriteLine(name);
}

У чому магія? Уся суть у тому, що всі сучасні колекції реалізують інтерфейс IEnumerable<T> — своєрідний «контракт», який гарантує, що колекція вміє повертати елементи по одному, послідовно.

Уявіть собі компанію, де для будь-якої колекції працівників (чи то відділ, команда проєкту, список учасників корпоративу) є правило: якщо об’єкт реалізує інтерфейс «IEnumerable», отже, ви завжди можете пройтися по всіх працівниках у певному порядку. Неважливо, «як вони зберігаються»; головне — що їх можна перебрати.

2. Інтерфейс IEnumerable<T> — що всередині?

Погляньмо, як виглядає цей інтерфейс у .NET:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

У ньому лише один методGetEnumerator, який повертає об’єкт типу IEnumerator<T>. Саме цей об’єкт відповідає за процес «переліку»: він знає, який елемент поточний, як перейти до наступного і коли все завершилося.

Підсумуємо: якщо клас (або колекція) реалізує IEnumerable<T>, отже, по ньому можна пройтися у циклі foreach, отримуючи елементи один за одним. І неважливо, як вони реалізовані всередині: хоч списком, хоч хеш-таблицею, хоч на жорсткому диску.

Міні-схема


Колекція (наприклад, List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T> (реальний «перебирач» елементів)

3. Практика та універсальність коду

Величезний плюс такого контракту — універсальність. Якщо функція або метод приймає IEnumerable<T>, це означає, що він сумісний з будь-якою колекцією — від масивів і списків до множин, черг і навіть власних колекцій користувача.

Наприклад, розгляньмо функцію підрахунку суми елементів:


// Можна рахувати суму будь-якого набору int, який підтримує IEnumerable<int>
int Sum(IEnumerable<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
        result += n;
    return result;
}

// Працює з List<int>
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));

// Працює з масивом!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));

// Працює навіть з результатом методу, що фільтрує елементи
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // Використовуємо LINQ

Приклад із життя: універсальні методи

Уявіть, що вам потрібно написати утиліту для пошуку найдовшого слова у будь-якій колекції рядків. Завдяки IEnumerable<string> ви зможете зробити це для будь-якого джерела — хоч масиву, хоч списку, хоч результату методів фільтрації тощо:


string FindLongest(IEnumerable<string> words)
{
    string longest = "";
    foreach (var word in words)
        if (word.Length > longest.Length)
            longest = word;
    return longest;
}

Його можна використовувати з будь-яким відповідним набором.

4. Чому цикл foreach працює з IEnumerable<T>?

Поширене запитання: чому цикл foreach «розуміє» будь-які колекції? Відповідь проста: компілятор C# шукає у класі метод GetEnumerator і очікує отримати об’єкт із методами MoveNext() і властивістю Current. Це і є стандартний інтерфейс — IEnumerator<T>.

Завдяки такому підходу ви можете навіть написати власний клас, який «видаватиме» елементи по черзі (наприклад, генератор послідовності чисел Фібоначчі), і, якщо він реалізує IEnumerable<int>, з ним можна працювати у foreach так само, як зі звичайним списком.

5. Що таке Enumerator і як він працює?

Усередині будь-якої колекції, що реалізує IEnumerable<T>, заховано спеціальний «перебирач» — Enumerator (науковою мовою: об’єкт, який реалізує інтерфейс IEnumerator<T>). Саме він подає елементи колекції по одному, коли ви пишете цикл foreach.

Інтерфейс IEnumerator<T>

Ось що вміє стандартний enumerator:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // Поточний елемент
    bool MoveNext();           // Перейти до наступного елемента
    void Reset();              // Повернутись на початок (використовується рідко)
}

Як це працює всередині?

  • MoveNext() — пересуває «вказівник» до наступного елемента і повертає true, якщо елемент є. Якщо елементів більше немає — повертає false.
  • Current — повертає поточний елемент (той, на який зараз вказує перебирач).
  • Reset() — скидає enumerator на початок (на практиці майже не використовується).
  • Dispose() — звільняє ресурси (потрібно для колекцій, які можуть працювати з файлами або мережею).

Приклад: як працює foreach «під капотом»

Коли ви пишете:


var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
    Console.WriteLine(n);

Насправді компілятор перетворює це приблизно на такий код:


var numbers = new List<int> { 1, 2, 3 };

// Отримуємо "перебирача"
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// Компілятор автоматично викликає Dispose() у блоці using (якщо enumerator реалізує IDisposable)

Важливо: якщо колекція працює із зовнішніми ресурсами (наприклад, файли, бази даних), enumerator може звільняти їх автоматично, коли обхід завершується.

Візуальна схема


Початок обходу -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> кінець обходу

6. IEnumerable і масиви, списки, множини: хто кому родич

Подивімося, які стандартні контейнери .NET реалізують цей інтерфейс:

Тип колекції Реалізує IEnumerable<T>? Можна пройти циклом foreach?
Масив (
int[]
)
List<T>
Dictionary<TKey, V>
✅ (за парами, ключами, значеннями)
HashSet<T>
Queue<T>
Stack<T>

Навіть рядок (string) реалізує звичайний IEnumerable, тож по кожному символу рядка можна так само пройтися.

7. Реалізація власного Enumerable

Цікаве завдання: спробуймо реалізувати мінімальну колекцію, яка зберігає парні числа в діапазоні від 0 до N і реалізує IEnumerable<int>. Тоді її можна буде обійти циклом і використовувати з LINQ.


// Клас-колекція, який можна "перебрати"
class EvenNumbers : IEnumerable<int>
{
    private int max;

    public EvenNumbers(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // Спеціальна магія для реалізації enumerator-а
    }

    // Явна реалізація не generic інтерфейсу IEnumerable для зворотної сумісності
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// Використання:
var evens = new EvenNumbers(10);
foreach(var e in evens)
    Console.Write($"{e} "); // 0 2 4 6 8 10

Зверніть увагу на ключову деталь: якщо ваш клас реалізує IEnumerable<T>, ви автоматично робите його сумісним із більшістю інструментів .NET: LINQ, foreach, а також методами, що приймають IEnumerable.

8. Типові граблі та нюанси

Іноді новачки думають, що IEnumerable<T> — це окрема колекція, у якої є елементи. Насправді це лише «обіцянка»: якщо почати перебір, елементи повертатимуться по одному.

Якщо потрібен випадковий доступ за індексом (myList[5]) або потрібно використовувати методи на кшталт Add чи Remove, інтерфейс IEnumerable<T> тут не допоможе. Він призначений лише для послідовного обходу!

Граблі: намагатися модифікувати колекцію під час перебору. Наприклад:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // НЕБЕЗПЕЧНО! InvalidOperationException
}

Краще спершу сформувати окремий список для видалення або використати методи, що створюють нову колекцію:


// Безпечний спосіб — створюємо нову колекцію вручну
var newList = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        newList.Add(item);
}
myList = newList;

// Або видаляємо за індексами у зворотному порядку
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ