Все разработчики C # в какой-то момент сталкивались с этими интерфейсами, но как их использовать? И лучше того, как их правильно использовать? - Потому что с этими ребятами я видел много ошибок.

Основным моментом при написании этой статьи является тот факт, что я потерял счет о том, сколько раз я видел ошибки и недопонимания, связанные с коллекциями C #, поэтому я надеюсь прояснить некоторые моменты здесь.

Прежде чем мы начнем, давайте вернемся немного назад и посмотрим на изображение в начале текста. Это визуализация того, как вы можете думать об IEnumerables.

Он работает как лента машины Тьюринга с курсором, указывающим на null. Этот курсор перемещается только прямо вперед (вправо на изображении), без произвольного доступа, и, кроме того, IEnumerable предоставляет только необходимую информацию для итерации коллекции.

Сказав это, мы можем приступить к программированию. Давайте проверим реализацию IEnumerable (с универсальными шаблонами и без них):

И это все. В реализации IEnumerable больше нет ни одной строчки.

Что это предлагает?

IEnumerable на самом деле является фабрикой IEnumerator . Теперь нам нужно проверить IEnumerator. Это немного сложнее, но мы справимся.

Существует два разных интерфейса, с использованием универсальных шаблонов и без них, как и IEnumerable.

IEnumerator - это тот, кто предоставляет информацию о том, как перебирать коллекцию (и IEnumerable, это самая известная, насколько это несправедливо?).

Фактически, когда мы используем цикл foreach, C # внутренне вызывает GetEnumerator коллекции, которую мы будем перебирать, и на каждом шаге вызывает метод MoveNext для перемещения курсора вперед.

Давайте запрограммируем очень простой код для итерации IEnumerable целых чисел с помощью цикла foreach:

Это довольно просто, не правда ли?

Как я уже сказал, этот код представляет собой простой синтаксический сахар для другого кода, немного более сложного, но все же простой фрагмент кода:

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

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

У метода MoveNext есть дизайн, который мне особенно не нравится. Он делает две разные вещи.

  1. Одна из этих вещей - переместить курсор в следующую позицию, что и является основной целью;
  2. Второстепенное - это то, что возвращает этот метод. Он возвращает true, если курсор указывает на допустимую позицию после его перемещения, или возвращает false, если нет.

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

После первого вызова метода MoveNext мы переместим курсор в самую первую позицию IEnumerable, как на изображении ниже:

А свойство Current просто возвращает значение в позиции, на которую указывает курсор.

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

Да, вы правильно прочитали, коллекция с всеми числами, что означает бесконечную коллекцию.

Вот кое-что, прежде чем мы начнем кодировать, весь смысл этой реализации состоит в том, чтобы увидеть, как IEnumerator и IEnumerable работают под капотом с циклом foreach, поэтому не принимайте это как лучшую возможную реализацию для новой коллекции, это просто эксперимент, да?

Начнем с IEnumerator, поэтому просто создайте новый класс, реализующий IEnumerator<int>. Он будет выглядеть, как показано ниже:

Итак, теперь нам нужно создать наши собственные вещи. Чтобы создать бесконечную коллекцию, мы будем использовать сам курсор в качестве значения.

Давайте создадим новое поле с именем _current, которое начинается с -1 и будет увеличиваться при вызове метода MoveNext (обычно значение сохраняется в IEnumerable, но это не обычный пример, помните?)

Хорошо, но давайте разберемся, почему _current начинается с -1 вместо нуля. Ответ довольно прост, потому что метод MoveNext вызывается перед итерацией.

Теперь перейдем к методу MoveNext, вы, наверное, уже знаете, что здесь делать. Нам нужно увеличить значение _current и всегда вернуть true, в конце концов, это бесконечная коллекция, поэтому всегда можно перейти к следующей.

Теперь нам не нужно беспокоиться о методе Dispose, поэтому давайте реализуем метод Reset, сбросив _current на его начальное значение.

Теперь давайте реализуем саму нашу коллекцию. Поверьте, это будет легко. Все, что нам нужно, это реализовать интерфейс IEnumerable<int> и вернуть экземпляр нашего IEnumerator.

Возможно, по какой-то причине вам может потребоваться возвращать новые экземпляры каждый раз, когда вы вызываете GetEnumerator, но для нашего примера достаточно только одного экземпляра.

Теперь мы можем использовать наш собственный класс внутри цикла foreach:

И мы можем получить следующее, следующее, следующее и продолжать делать это вечно.

Как это круто?

Значит, все было реализовано правильно? - Не совсем.

Давайте принудительно прервем прерывание, используя break:

BANG! - Возникло исключение.

Почему было выброшено это исключение? - Просто метод Dispose еще не реализован.

Верно, но когда это называется?

Сразу после петли! Чтобы запомнить, давайте повторно реализуем его без синтаксиса сахара foreach, другими словами, используя цикл while:

Теперь все прояснилось, не так ли?

Когда мы перебираем IEnumerable, мы используем ключевое слово using, и, как вы, вероятно, уже знаете, метод Dispose вызывается в конце области using.

Причина, по которой вызывается метод Dispose, намного яснее с этим синтаксисом, хотя он выглядит хуже. Мы можем исправить это, просто вызвав метод Reset по адресу Dispose.

Предупреждение

Это вообще не лучший способ реализовать метод Dispose, это просто эксперимент, хорошо?

Теперь у вас есть собственная бесконечная коллекция, пора щелкнуть пальцами (как это сделал Танос)!

Увидимся в следующем посте!