Что такое IEnumerable и IEnumerator в C#?

Что такое IEnumerable и IEnumerator в C#? Я много искал в интернете информации об этом, но везде объясняется слишком научно и непонятно. Можете объяснить, как это работает, как реализуется и, собственно, зачем это нужно так, чтобы было понятно и, если получится, с примерами?


Ответы (3 шт):

Автор решения: Bulson

По простому, т.е. ненаучно.

Если класс реализует IEnumerable (Перечисляемый), то у него есть метод GetEnumerator() (ПолучитьПеречислитель),который возвращает экземпляр класса реализующего IEnumerator.

Если класс реализует IEnumerator (Перечислитель), то у него есть методы MoveNext() (ПерейтиКСледующему), Reset() (Сбросить или ВернутсяКПервому), свойство Current (Текущий элемент).

Применяется для классов коллекций, чтоб их можно было перебирать с помощью циклической конструкции foreach.

→ Ссылка
Автор решения: Sergey K.

C# - это ООП язык. Поэтому коллекции (List, Set, Dictionary и прочие) следуют концепции наследования и полиморфизма.

Что это значит. Все эти классы объединяет то, что они хранят какие-то значения, которые можно перечислять (Enumerate), значит, они перечисляемые (Enumerable), это означает, что из них можно доставать одно значение за другим по очереди.

Для того, чтобы удобно работать с перечислением, то есть, поочерёдно вытаскивать значения, есть классы перечисляторы - Enumerator. Эти классы Enumerator'ы реализуют методы, которые помогут перейти к следующему элементу при перечислении MoveNext() или достать данные из нынешней позиции Current().

Теперь, когда, надеюсь, стало понятно что это такое Enumerable и Enumerator, посмотрим, зачем нужны интерфейсы для них.

Интерфейсы - это абстрактные классы, которые не могут хранить реализацию методов, то есть, всё что у них есть должно быть реализовано (описано по-своему) в дочерних классах. В нашем случае, классы коллекций для того, чтобы в любом месте кода было известно, что они все перечисляемые и у них есть метод GetEnumerator(), который отдаст тебе нужный Enumerator мы наследуем от IEnumerable. Таким образом не важно, что у нас лежит Set, List или Dictionary или иной любой перечисляемый класс, мы всегда знаем как получить его Enumerator.

Когда мы получили enumerator, то всегда знаем как можем перебрать все элементы один за одним, не зависимо от того, с какой коллекцией мы работаем.

→ Ссылка
Автор решения: VladD

Смотрите.

У многих современных языков есть понятие «автоматического обхода коллекции». Например, у C++ есть кроме обычного цикла с индексами также и цикл for (T variable : collection), который обходит в том числе и неиндексируемые коллекции. У Java есть такой же цикл.

Ценность такого цикла состоит в том, что вы можете «обходить» любую коллекцию, не заботясь о том, как же это сделать: например, для массива это просто обход по индексу, для структур, основанных на дереве — это обход дерева и. т. д.

Можно было бы захардкодировать методы обхода известных коллекций, но это слабо расширяемое решение, т. к. оно не позволяет создавать кастомные, ваши коллекции. А также компилятор приходилось бы обновлять каждый раз, когда стандартная библиотека добавляла бы в себя новую коллекцию. Поэтому такая идея не проходит, и нужно придумать общее решение, не зависящее от конкретной коллекции.

Итак, нам нужна абстракция, которая говорит «объекты этого типа представляют собой набор элементов, который можно обходить циклом наподобие for». В OOP-языках такого рода абстракции традиционно представляются в виде интерфейсов. Поэтому и появился интерфейс IEnumerable<T> (где T — тип элемента коллекции), означающий перечислимый контейнер (то есть тот, который можно обойти циклом).

Было решено, что для обхода будет применяться следующая языковая конструкция:

foreach (T current in collection)
{
    // обработать элемент current
}

Как можно это реализовать? Язык может опираться лишь на интерфейс IEnumerable<T>, значит, нужные методы должны быть «зашиты» в интерфейс IEnumerable<T>. Поскольку коллекцию могут обходить одновременно несколько циклов (например, так:

foreach (int x in collection)
    foreach (int y in collection)
        Console.WriteLine("({x}, {y})");

вполне можно написать, а ещё можно одновременно обходить коллекцию из разных потоков), то нам нужно знать, в каком месте находится каждый из одновременных обходов коллекции.

Это самое состояние обхода должно быть как-то представлено, и за это, как было решено, отвечает интерфейс IEnumerator<T>. Он содерждит для массива — текущий индекс в данном обходе, для дерева — указатель на текущий узел дерева, для более сложных случаев — более сложные вещи. Каждая коллекция «придумывает» себе подходящий IEnumerator<T>. Интерфейс IEnumerable<T>, таким образом, получается очень простым: он содержит лишь функцию GetEnumerator(), которая возвращает энумератор, который представляет данные для конкретного обхода. Цикл foreach должен получить энумератор, и дальше работать только с ним.

Теперь, энумератор. Он отвечает за один конкретный обход, и ему достаточно содержать две функции: MoveNext() для перехода к следующему элементу, и Current для получения текущего элемента.

Таким образом, энумерация выглядит приблизительно так:

var enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
   var current = enumerator.Current;
   // обработать элемент current 
}

Вот во что под капотом превращается цикл foreach.

Вот таким образом интерфейсы IEnumerable<T> и IEnumerator<T> позволяют реализовать энумерацию произвольного класса. Вот вам простой пример реализации (я убрал все проверки, чтобы код был проще):

class CustomEnumerable : IEnumerable<int>
{
    readonly int[] data; // просто массив, который можно обходить
    public CustomEnumerable(int[] data) => this.data = data;

    // отдаём сконструированный нами объект
    public IEnumerator<int> GetEnumerator() => new EnumeratorImplementation(this);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    // этот класс отвечает за одну энумерацию объекта CustomEnumerable 
    class EnumeratorImplementation : IEnumerator<int>
    {
        readonly CustomEnumerable self; // ссылка на сам объект
        public EnumeratorImplementation(CustomEnumerable self) => this.self = self;

        int currIndex = -1; // текущий индекс в энумерации
        bool isIndexValid => currIndex >= 0 && currIndex < self.data.Length;
        public int Current => isIndexValid ? self.data[currIndex] : 0;
        object IEnumerator.Current => Current;
        public bool MoveNext() // переход к следующему:
        {
            // если мы уже прошли до конца, больше увеличивать индекс не надо
            if (currIndex < self.data.Length)
                currIndex++;
            return isIndexValid; // true если мы ещё не дошли до конца
        }

        public void Reset() => currIndex = -1;
        public void Dispose() { }
    }
}
→ Ссылка