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-а
    }

    // Явная реализация не обобщенного интерфейса 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, методами, которые принимают enumerable.

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);
}
2
Задача
C# SELF, 28 уровень, 1 лекция
Недоступна
Реализация собственной коллекции, поддерживающей IEnumerable<int>
Реализация собственной коллекции, поддерживающей IEnumerable<int>
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ