Мы попытаемся ответить на поставленный выше вопрос с помощью ряда подвопросов. Надеюсь, к концу этой статьи мы будем лучше понимать интерфейс IEnumerable.

Почему мы должны реализовать интерфейс IEnumerable?

Обеспечить более простой способ перечисления пользовательских типов.

Допустим, у нас есть собственный класс OddNumbers, единственной целью которого является инкапсуляция массива первых n нечетных чисел.

Теперь, если бы нам нужно было перебрать массив нечетных чисел за пределами этого класса, нам пришлось бы открыть внутренний массив, предоставив метод или свойство, которое возвращает массив.

В некоторых сценариях это нормально, но что, если мы не хотим раскрывать внутренний массив? Существует ли еще способ перебора массива?

Вот здесь и появляется интерфейс IEnumerable.

Как реализовать интерфейс IEnumerable?

Мы можем реализовать в нашем классе интерфейс IEnumerable, который сообщает компилятору, что экземпляр нашего класса может быть повторен через цикл foreach.

Для реализации интерфейса IEnumerable требуется реализация метода GetEnumerator(). Этот метод возвращает объект IEnumerator, который является дескриптором, используемым для итерации экземпляра нашего класса.

И это почти все!! Это охватывает основы того, как реализовать интерфейс IEnumerable.

Но давайте копнем немного глубже, чтобы понять больше.

Реализация интерфейса IEnumerable и IEnumerable‹T›

В предыдущем примере наш класс реализовал необобщенный интерфейс IEnumerable. В результате метод GetEnumerator() вернул экземпляр неуниверсального IEnumerator.

Если бы наш класс реализовал универсальный интерфейс IEnumerable‹T›, метод GetEnumerator() должен был бы вернуть экземпляр универсального IEnumerator‹T›.

Так в чем же разница и что лучше использовать?

Когда тип возвращаемого значения не является универсальным IEnumerator, мы на самом деле не указываем, каков тип данных каждого члена последовательности, по которой выполняется итерация. Следовательно, по умолчанию используется объект. Это нормально, если мы знаем тип данных при итерации через цикл foreach.

Но если мы не знали этой информации и использовали var, что-то может пойти не так.

ошибка CS0019: оператор «*» не может быть применен к операндам типа «объект» и «целое»

Мы можем исправить это, реализовав в нашем классе универсальный интерфейс IEnumerable‹T›.

Теперь можно использовать var в foreach для перебора последовательности.

var теперь может определить тип как int, а не как объект.

Поэтому всегда лучше реализовать интерфейс IEnumerable‹T›, поскольку он безопасен для типов.

Реализация интерфейса IEnumerable не является абсолютно необходимой

Это может показаться странным после всех разговоров о реализации IEnumerable. Но важно, чтобы мы поняли это, прежде чем двигаться дальше. Нашему классу не обязательно реализовывать интерфейс IEnumerable (или IEnumerable‹T›) для foreach для итерации по экземпляру нашего класса. Когда foreach пытается выполнить итерацию экземпляра нашего класса, он ищет метод GetEnumerator() в нашем классе, который возвращает экземпляр IEnumerator. Это единственное требование, не реализующее интерфейс IEnumerable.

Наш класс вполне мог бы не реализовывать IEnumerable, но предоставить реализацию метода IEnumerator GetEnumerator();, и мы все равно могли бы использовать foreach для перебора экземпляра нашего класса.

Тем не менее, для наших классов всегда полезно реализовать интерфейс IEnumerable (или IEnumerable‹T›), потому что это проясняет намерение и заставляет разработчиков реализовывать метод GetEnumerator(), а не помнить об этом.

Еще одна причина для реализации IEnumerable — возможность передавать экземпляры нашего класса методам, которые ожидают IEnumerable. Это всегда удобно.

Что такое «доходность» в нашем методе GetEnumerator()?

Здесь нам нужно понять, что использование yield в нашем методе GetEnumerator() не является абсолютной необходимостью. Основное требование этого метода — вернуть экземпляр IEnumerator. Мы могли бы создать это вручную (как мы вскоре увидим). Ключевое слово yield просто заставляет компилятор C# делать это за нас.

Из документов MSDN:

Использование yield для определения итератора устраняет необходимость в явном дополнительном классе (классе, который содержит состояние для перечисления) при реализации шаблона IEnumerable и IEnumerator для пользовательского типа коллекции.

Далее мы рассмотрим «явный дополнительный класс», о котором они упоминали выше.

Создание счетчика вручную

Создайте еще один класс под названием «OddNumberEnumerator» и реализуйте в нем интерфейс IEnumerator‹T›. Это сообщает компилятору C#, что экземпляр этого класса может действовать как дескриптор, который можно использовать для итерации последовательности.

Сама логика, связанная с итерацией, выражается через реализацию метода MoveNext() и свойства Current.

Свойство Current, доступное только для чтения, удерживает текущий элемент, просматриваемый в процессе итерации.

MoveNext() возвращает логическое значение, уведомляющее можем ли мы продвинуться вперед в итерации или нет. Если мы сможем продвинуться вперед в итерации, она также обновит значение свойства «Текущий».

Вот весь класс

Осталось только обновить метод GetEnumerator() в нашем классе OddNumbers, чтобы он возвращал экземпляр класса OddNumberEnumerator.

После выполнения вышеуказанного мы все еще можем использовать цикл foreach для итерации экземпляра нашего класса «OddNumbers».

Важно понимать, что ключевое слово «yield» создает для нас перечислитель, и почти всегда лучше использовать yield, если только нам не нужно добавлять пользовательскую логику для процесса итерации.

Также можно выполнить итерацию с помощью перечислителя

И последнее, прежде чем мы подпишем. В обоих сценариях (используя yield или создавая собственный перечислитель вручную) мы можем использовать перечислитель для итерации экземпляра нашего класса. Нам не нужно использовать цикл foreach.

Опять же, всегда лучше использовать цикл foreach, поскольку он абстрагирует ручное использование перечислителей и значительно упрощает нашу жизнь.

И это пока все, что касается IEnumerable.