Зачем нам нужны два интерфейса для перечисления коллекции?

Уже давно я пытаюсь понять идею, лежащую в основе IEnumerable а также IEnumerator, Я прочитал все вопросы и ответы, которые смог найти в сети, и в частности в Stackru, но я не удовлетворен. Я дошел до того, что понимаю, как использовать эти интерфейсы, но не понимаю, почему они используются таким образом.

Я думаю, что суть моего недоразумения состоит в том, что нам нужны два интерфейса для одной операции. Я понял, что, если оба необходимы, одного, вероятно, было недостаточно. Поэтому я взял "жестко закодированный" эквивалент foreach (как я нашел здесь):

while (enumerator.MoveNext())
{
    object item = enumerator.Current;

    // logic
}

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

Поэтому я создал класс коллекции и реализовал IForeachable:

class Collection : IForeachable
{
    private int[] array = { 1, 2, 3, 4, 5 };
    private int index = -1;

    public int Current => array[index];

    public bool MoveNext()
    {
        if (index < array.Length - 1)
        {
            index++;
            return true;
        }

        index = -1;
        return false;
    }
}

и использовал foreach эквивалент номинирования коллекции:

var collection = new Collection();

while (collection.MoveNext())
{
    object item = collection.Current;

    Console.WriteLine(item);
}

И это работает! Так чего здесь не хватает, что делает необходимым еще один интерфейс?

Благодарю.


Изменить: Мой вопрос не является дубликатом вопросов, перечисленных в комментариях:

  • Этот вопрос, почему интерфейсы необходимы для перечисления в первую очередь.
  • Этот вопрос и этот вопрос о том, что это за интерфейсы и как их использовать.

Мой вопрос: почему они спроектированы такими, какие они есть, а не то, что они, как они работают, и зачем они нам нужны в первую очередь.

2 ответа

Решение

Каковы два интерфейса и что они делают?

Интерфейс IEnumerable размещается в объекте коллекции и определяет метод GetEnumerator(), который возвращает (обычно новый) объект, который реализует интерфейс IEnumerator. Оператор foreach в C# и оператор For Each в VB.NET используют IEnumerable для доступа к перечислителю, чтобы перебрать элементы в коллекции.

Интерфейс IEnumerator - это контракт, заключенный с объектом, который фактически выполняет итерацию. Он сохраняет состояние итерации и обновляет его по мере того, как код перемещается по коллекции.

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

Ничто не мешает IEnumerator и IEnumerable быть реализованными в одном классе. Тем не менее, за это есть штраф - невозможно иметь два или более циклов в одной коллекции одновременно. Если можно быть абсолютно уверенным в том, что когда-либо не будет необходимости циклически повторять коллекцию дважды, это нормально. Но в большинстве случаев это невозможно.

Когда кто-то будет перебирать коллекцию более одного раза за раз?

Вот два примера.

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

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

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

- Это было из сообщения в блоге, которое я написал много лет назад: https://colinmackay.scot/2007/06/24/iteration-in-net-with-ienumerable-and-ienumerator/

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

С другой стороны, потому что IEnumerable возвращается новый IEnumerator для каждого вызывающего - вы можете иметь несколько перечислений одновременно, потому что у каждого вызывающего есть свое собственное состояние перечисления. Я думаю, что одной этой причины достаточно, чтобы оправдать два интерфейса. Перечисление - это, по сути, операция чтения, и было бы очень запутанно, если бы вы не могли читать одно и то же одновременно в нескольких местах.

Другие вопросы по тегам