Зачем нам нужны два интерфейса для перечисления коллекции?
Уже давно я пытаюсь понять идею, лежащую в основе 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
для каждого вызывающего - вы можете иметь несколько перечислений одновременно, потому что у каждого вызывающего есть свое собственное состояние перечисления. Я думаю, что одной этой причины достаточно, чтобы оправдать два интерфейса. Перечисление - это, по сути, операция чтения, и было бы очень запутанно, если бы вы не могли читать одно и то же одновременно в нескольких местах.