Когда мне следует использовать IEnumerator для зацикливания в С#?

Мне было интересно, есть ли случаи, когда выгодно использовать IEnumerator вместо цикла foreach для перебора коллекции? Например, есть ли время, когда было бы лучше использовать один из следующих примеров кода вместо другого?

IEnumerator<MyClass> classesEnum = myClasses.GetEnumerator();
while(classesEnum.MoveNext())
    Console.WriteLine(classesEnum.Current);

вместо

foreach (var class in myClasses)
    Console.WriteLine(class);

person lomaxx    schedule 19.01.2009    source источник


Ответы (4)


Во-первых, обратите внимание, что одно большое различие в вашем примере (между foreach и GetEnumerator) состоит в том, что foreach гарантирует вызов Dispose() на итераторе, если итератор IDisposable. Это важно для многих итераторов (например, которые могут использовать внешний поток данных).

На самом деле бывают случаи, когда foreach не так полезен, как хотелось бы.

Во-первых, обсуждается случай «первого элемента» здесь (foreach/обнаружение первой итерации).

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

static IEnumerable<T> Zip<T>(this IEnumerable<T> left,
    IEnumerable<T> right)
{
    using (var iter = right.GetEnumerator())
    {
        // consume everything in the first sequence
        foreach (var item in left)
        {
            yield return item;

            // and add an item from the second sequnce each time (if we can)
            if (iter.MoveNext())
            {
                yield return iter.Current;
            }
        }
        // any remaining items in the second sequence
        while (iter.MoveNext())
        {
            yield return iter.Current;
        }                
    }            
}

static bool SequenceEqual<T>(this IEnumerable<T> left,
    IEnumerable<T> right)
{
    var comparer = EqualityComparer<T>.Default;

    using (var iter = right.GetEnumerator())
    {
        foreach (var item in left)
        {
            if (!iter.MoveNext()) return false; // first is longer
            if (!comparer.Equals(item, iter.Current))
                return false; // item different
        }
        if (iter.MoveNext()) return false; // second is longer
    }
    return true; // same length, all equal            
}
person Marc Gravell    schedule 19.01.2009
comment
Разве это не должно быть что-то вроде while(iter.MoveNext()) в случае, если справа два или более элементов, чем слева? - person Spoike; 19.01.2009
comment
Привет Марк, очень хороший ответ! Не могли бы вы уточнить, что вы подразумеваете под выполнением перекрестного соединения? - person M.Y. Babt; 14.05.2016
comment
@Nameless, возможно, я злоупотребляю терминологией SQL, но под перекрестным соединением я просто подразумеваю декартово произведение между двумя наборами. - person Marc Gravell; 15.05.2016
comment
@MarcGravell Спасибо за разъяснение! - person M.Y. Babt; 15.05.2016

Согласно спецификации языка C#:

Оператор foreach формы

foreach (V v in x) embedded-statement

затем расширяется до:

{
    E e = ((C)(x)).GetEnumerator();
    try {
            V v;
            while (e.MoveNext()) {
                    v = (V)(T)e.Current;
                    embedded-statement
            }
    }
    finally {
            ... // Dispose e
    }
}

Суть ваших двух примеров одинакова, но есть некоторые важные различия, в первую очередь try/finally.

person Jay Bazuzi    schedule 19.01.2009
comment
Интересно, потому что это также показывает, какие неявные преобразования (необязательно определяемые пользователем) могут быть запущены. - person sehe; 08.11.2012

В C# foreach делает то же самое, что и код IEnumerator, который вы разместили выше. Конструкция foreach предназначена для облегчения работы программиста. Я считаю, что IL, сгенерированный в обоих случаях, одинаков/похож.

person Andy    schedule 19.01.2009
comment
Есть случаи, когда вам нужно сделать это вручную - см. мой ответ ниже. - person Marc Gravell; 19.01.2009
comment
На самом деле нет, IL не идентичен в его образце, потому что он не проверил итератор на наличие IDisposable. Это важно для многих итераторов. - person Marc Gravell; 19.01.2009

Я использовал его иногда, когда первая итерация делала что-то отличное от других.

Например, если вы хотите вывести участников на консоль и разделить результаты по строкам, вы можете написать.

using (IEnumerator<MyClass> classesEnum = myClasses.GetEnumerator()) {
    if (classEnum.MoveNext())
        Console.WriteLine(classesEnum.Current);
    while (classesEnum.MoveNext()) {
        Console.WriteLine("-----------------");
        Console.WriteLine(classesEnum.Current);
    }
}

Тогда результат

myClass 1
-----------------
myClass 2
-----------------
myClass 3

И еще одна ситуация, когда я перебираю 2 счетчика вместе.

using (IEnumerator<MyClass> classesEnum = myClasses.GetEnumerator()) {
    using (IEnumerator<MyClass> classesEnum2 = myClasses2.GetEnumerator()) {
        while (classesEnum.MoveNext() && classEnum2.MoveNext())
            Console.WriteLine("{0}, {1}", classesEnum.Current, classesEnum2.Current);
    }
}

результат

myClass 1, myClass A
myClass 2, myClass B
myClass 3, myClass C

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

person Chaowlert Chaisrichalermpol    schedule 19.01.2009
comment
Первая итерация: вы можете использовать foreach(var item in myList.Select(val, index)) для получения индекса текущего элемента. С помощью метода расширения вы даже можете написать foreach(var item in myList.SelectWithIndex()) - person Sylvain Rodrigue; 05.12.2020