Выиграют ли C# различия между типами перечислителей, такими как итераторы C++?

Я думал о методе IEnumerator.Reset(). Я читал в документации MSDN, что это только для COM-взаимодействия. Как программист на C++, мне кажется, что IEnumerator, поддерживающий Reset, я бы назвал прямой итератор, тогда как IEnumerator, который не поддерживает Reset, на самом деле является итератором ввода .

Итак, первая часть моего вопроса: правильно ли это понимание?

Вторая часть моего вопроса: было бы полезно в C#, если бы проводилось различие между итераторами ввода и итераторами вперед (или «перечислителями», если хотите)? Не поможет ли это устранить некоторую путаницу среди программистов, подобную той, что найдена в этом SO вопрос о клонировании итераторов< /а>?

РЕДАКТИРОВАТЬ: уточнение по итераторам вперед и ввода. Итератор ввода гарантирует, что вы можете перечислить элементы коллекции (или из функции-генератора, или из входного потока) только один раз. Именно так работает IEnumerator в C#. Возможность повторного перечисления определяется тем, поддерживается ли Reset. Прямой итератор не имеет этого ограничения. Вы можете перечислять членов так часто, как хотите.

Некоторые программисты на C# не понимают, почему нельзя надежно использовать IEnumerator в многопроходном алгоритме. Рассмотрим следующий случай:

void PrintContents(IEnumerator<int> xs)
{
  while (iter.MoveNext())
    Console.WriteLine(iter.Current); 
  iter.Reset();
  while (iter.MoveNext())
    Console.WriteLine(iter.Current); 
}

Если мы вызовем PrintContents в этом контексте, нет проблем:

List<int> ys = new List<int>() { 1, 2, 3 }
PrintContents(ys.GetEnumerator()); 

Однако посмотрите на следующее:

IEnumerable<int> GenerateInts() {   
  System.Random rnd = new System.Random();
  for (int i=0; i < 10; ++i)
    yield return Rnd.Next();
}

PrintContents(GenerateInts());

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


person cdiggins    schedule 28.10.2009    source источник
comment
Я думаю, вы имеете в виду IEnumerator.Reset, а не IEnumerable<T>.Reset, верно?   -  person Doug McClean    schedule 28.10.2009
comment
Да спасибо! Извини за это.   -  person cdiggins    schedule 28.10.2009
comment
Интересный вопрос. Но, возможно, вам следует немного объяснить язык С++, так как многие программисты на С# увидят это. Может быть неочевидно, что такое итератор ввода и прямой итератор (в частности, их однократные/многопроходные возможности, которые действительно имеют отношение к этому вопросу)   -  person jalf    schedule 28.10.2009


Ответы (5)


Интересный вопрос. Я считаю, что C#, конечно, полезен. Однако добавить будет непросто.

Различие существует в C++ из-за его гораздо более гибкой системы типов. В C# у вас нет надежного универсального способа клонирования объектов, необходимого для представления прямых итераторов (для поддержки многопроходной итерации). И, конечно же, чтобы это было действительно полезно, вам также необходимо поддерживать двунаправленные итераторы/перечислители с произвольным доступом. И чтобы заставить их все работать гладко, вам действительно нужна какая-то форма утиного набора, как в шаблонах C++.

В конечном счете, объемы этих двух понятий различны.

Предполагается, что в C++ итераторы представляют все, что вам нужно знать о диапазоне значений. Учитывая пару итераторов, мне нужен исходный контейнер. Я могу сортировать, я могу искать, я могу манипулировать и копировать элементы столько, сколько захочу. Оригинальный контейнер отсутствует на фото.

В C# перечислители не предназначены для таких задач. В конечном счете, они просто предназначены для того, чтобы вы могли проходить последовательность линейным образом.

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

К несчастью.

person jalf    schedule 28.10.2009
comment
После одной чашки кофе я не понимаю, почему клонирование необходимо для многопроходного итератора (игнорируя полные требования прямого итератора) на данный момент. - person cdiggins; 28.10.2009
comment
Как еще можно выполнить несколько проходов прямого итератора? Он не двунаправленный, поэтому вы не можете вернуться туда, где вы были, и повторить итерацию снова. Вы должны создать копию итератора, чтобы у вас было два итератора, указывающих на одно и то же место в последовательности, а затем их можно было бы увеличивать по отдельности. - person jalf; 28.10.2009
comment
Если вы думаете, что метод Reset() может заменить клонирование, то да, вроде как. За исключением того, что сброс возвращает вас только к одной фиксированной точке последовательности (начало). Но прямой итератор должен иметь возможность выполнять несколько проходов по подмножеству данных (скажем, только по последним трем элементам). Сброс тут особо не поможет. (или, по крайней мере, это было бы смехотворно медленно, если бы вам пришлось делать полный сброс и проходить всю последовательность только для того, чтобы вернуться туда, где вы хотели выполнить многопроходный обход) - person jalf; 28.10.2009
comment
Добавление различия перечислителей принесет пользу только в очень небольшом числе случаев использования. Стоимость усложняет все варианты использования. IMO, это было хорошее решение (преднамеренное или нет) оставить сложности итераций С++ вне С# и сделать их простыми. - person kaalus; 05.07.2012

Reset было большой ошибкой. Я звоню в махинации на Reset. На мой взгляд, правильный способ отразить различие, которое вы проводите между «прямыми итераторами» и «итераторами ввода» в системе типов .NET, заключается в различии между IEnumerable<T> и IEnumerator<T>.

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

person Doug McClean    schedule 28.10.2009
comment
+1 за ссылки. Однако я не согласен с вашей аналогией. IEnumerable больше похож на контейнер C++ sgi.com/tech/stl/Container.html< /а> - person cdiggins; 28.10.2009

С точки зрения C#:

Вы почти никогда не используете IEnumerator напрямую. Обычно вы делаете оператор foreach, который ожидает IEnumerable.

IEnumerable _myCollection;
...
foreach (var item in _myCollection) { /* Do something */ }

Вы тоже не проходите мимо IEnumerator. Если вы хотите передать коллекцию, которая нуждается в итерации, вы передаете IEnumerable. Поскольку IEnumerable имеет единственную функцию, которая возвращает IEnumerator, ее можно использовать для повторения коллекции несколько раз (множественные проходы).

Нет необходимости в функции Reset() для IEnumerator, потому что, если вы хотите начать все сначала, вы просто выбрасываете старую (собранный мусор) и получаете новую.

person Matt Brunell    schedule 28.10.2009

Платформа .NET получила бы огромную пользу, если бы существовала возможность спрашивать IEnumerator<T> о том, какие возможности он может поддерживать и какие обещания может давать. Такие функции также были бы полезны в IEnumerable<T>, но возможность задавать вопросы перечислителю позволила бы коду, который может получать перечислитель от оболочек, таких как ReadOnlyCollection, использовать базовую коллекцию более совершенными способами, не привлекая оболочку.

Имея любой перечислитель для коллекции, которая может быть пронумерована полностью и не слишком велика, можно создать из него IEnumerable<T>, который всегда будет давать одну и ту же последовательность элементов (в частности, набор элементов, оставшихся в перечислителе) путем чтение всего его содержимого в массив, размещение и отбрасывание перечислителя и получение перечислителей из массива (используя его вместо исходного заброшенного перечислителя), обертывание массива в ReadOnlyCollection<T> и его возврат. Хотя такой подход будет работать с любой перечислимой коллекцией, удовлетворяющей вышеуказанным критериям, он будет ужасно неэффективен для большинства из них. Наличие средств, запрашивающих у перечислителя оставшееся содержимое в неизменном IEnumerable<T>, позволило бы многим типам перечислителей выполнять указанное действие намного эффективнее.

person supercat    schedule 12.05.2015

Я так не думаю. Я бы назвал IEnumerable прямым итератором и входным итератором. Это не позволяет вам вернуться назад или изменить базовую коллекцию. С добавлением ключевого слова foreach большую часть времени об итераторах практически не думают.

Мнение: разница между итераторами ввода (получить каждый) и итераторами вывода (сделать что-то с каждым) слишком тривиальна, чтобы оправдать добавление к фреймворку. Кроме того, чтобы сделать выходной итератор, вам нужно передать делегата итератору. Итератор ввода кажется более естественным для программистов на C#.

Также есть IList<T>, если программисту нужен произвольный доступ.

person Matt Brunell    schedule 28.10.2009
comment
В С++ прямой итератор поддерживает многопроходные алгоритмы, это не то, что вы можете надежно сделать с помощью IEnumerable/IEnumerator. Например: если IEnumerable был сгенерирован из функции yield. - person cdiggins; 28.10.2009
comment
Я не понимаю, почему IEnumerable не поддерживает многопроходные алгоритмы. Пользовательский итератор (использующий yield return) создает экземпляр IEnumerator. У вас может быть несколько объектов IEnumerator, одновременно перебирающих одну и ту же коллекцию. - person Matt Brunell; 28.10.2009
comment
Но, имея только IEnumerator, вы не можете выполнить более одного прохода по коллекции. Вам необходимо иметь доступ к базовой коллекции для выполнения многопроходной итерации, что является небольшим недостатком. (Поскольку в С++ итераторы предназначены для удовлетворения всех ваших потребностей в итерации. В С# некоторые довольно простые алгоритмы (все, что требует более одного прохода) должны нарушать абстракцию и требовать доступа к базовому контейнеру) - person jalf; 28.10.2009
comment
Также просто к вашему сведению (это, вероятно, должно было быть объяснено в вопросе, поскольку его будут читать многие программисты, не работающие на C++): на языке C++ input iterator — это итератор, который поддерживает одиночный -pass обход и предоставляет неизменяемые значения. Но на самом деле важным является однопроходный аспект. Вы можете в основном рассматривать его как оболочку потока. Он не поддерживает идею копирования состояния итераторов, поэтому многопроходная обработка невозможна. Прямой итератор можно копировать, поэтому он может выполнять несколько проходов. - person jalf; 28.10.2009