Странное поведение Enumerator.MoveNext()
Может кто-нибудь объяснить, почему этот код работает в бесконечном цикле? Почему MoveNext()
возвращает true
всегда?
var x = new { TempList = new List<int> { 1, 3, 6, 9 }.GetEnumerator() };
while (x.TempList.MoveNext())
{
Console.WriteLine("Hello World");
}
Ответы
Ответ 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
, а не с фиктивным поведением. Однако я не знаю конкретных планов добавления такого атрибута.