Подтвердить что ты не робот

Странное поведение Enumerator.MoveNext()

Может кто-нибудь объяснить, почему этот код работает в бесконечном цикле? Почему MoveNext() возвращает true всегда?

var x = new { TempList = new List<int> { 1, 3, 6, 9 }.GetEnumerator() };
while (x.TempList.MoveNext())
{
  Console.WriteLine("Hello World");
}
4b9b3361

Ответ 1

List<T>.GetEnumerator() возвращает измененный тип значения (List<T>.Enumerator). Вы сохраняете это значение в анонимном типе.

Теперь давайте посмотрим, что это делает:

while (x.TempList.MoveNext())
{
    // Ignore this
}

Это эквивалентно:

while (true)
{
    var tmp = x.TempList;
    var result = tmp.MoveNext();
    if (!result)
    {
        break;
    }

    // Original loop body
}

Теперь обратите внимание на то, что мы вызываем MoveNext() on - копия значения, которое находится в анонимном типе. Вы не можете изменить значение в анонимном типе - все, что у вас есть, - это свойство, которое вы можете вызвать, которое даст вам копию значения.

Если вы измените код на:

var x = new { TempList = (IEnumerable<int>) new List<int> { 1, 3, 6, 9 }.GetEnumerator() };

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

Для анализа в очень похожей ситуации (опять же с использованием List<T>.GetEnumerator()) см. мое сообщение в блоге 2010 года "Итерайте, черт вас побери!" .

Ответ 2

В то время как конструкция foreach в С# и цикле For Each в VB.NET часто используется с типами, которые реализуют IEnumerable<T>, они будут принимать любой тип, который включает метод GetEnumerator, возвращающий тип которого обеспечивает подходящий MoveNext и свойство Current. При возврате типа GetEnumerator тип значения во многих случаях позволит реализовать foreach более эффективно, чем это возможно, если оно вернет IEnumerator<T>.

К сожалению, поскольку нет способа, с помощью которого тип может предоставлять перечислитель типа значения при вызове из foreach, не поставляя его при вызове метода GetEnumerator, авторы List<T> столкнулись с небольшим компромисс между эффективностью и семантикой. В то время, поскольку С# не поддерживал вывод типа переменной, любой код, использующий значение, возвращаемое из List<T>.GetEnumerator, должен был бы объявить переменную типа IEnumerator<T> или List<T>.Enumerator. Код, использующий прежний тип, будет вести себя так, как если бы List<T>.Enumerator был ссылочным типом, и программист, использующий последний, мог предположить, что он является типом структуры. Однако, когда С# добавил тип вывода, это предположение перестало удерживаться. Код может очень легко закончиться с использованием типа List<T>.Enumerator, если программист не знает о существовании этого типа.

Если бы С# когда-либо определяли атрибут struct-method, который мог бы использоваться для тегов методов, которые не должны быть invokable в структурах только для чтения, и если List<T>.Enumerator использовал его, код, такой как ваш, мог бы правильно уступить ошибка времени компиляции при вызове MoveNext, а не с фиктивным поведением. Однако я не знаю конкретных планов добавления такого атрибута.