Ответ 1
Здесь недостаток этой проблемы. Далее следует более подробное объяснение.
-
List<T>.GetEnumerator()
возвращает struct, тип значения. - Эта структура изменчива (всегда рецепт катастрофы)
- Когда присутствует
using () {}
, структура хранится в поле лежащего в основе сгенерированного класса для обработки частиawait
. - При вызове
.MoveNext()
через это поле копия значения поля загружается из базового объекта, поэтому он как быMoveNext
никогда не вызывался, когда код читает.Current
Как отметил Марк в комментариях, теперь, когда вы знаете о проблеме, простое "исправление" заключается в том, чтобы переписать код, чтобы явным образом заблокировать структуру, это позволит убедиться, что изменяемая структура является той же самой, что и везде в этом коде, вместо новых копий, которые мутируют повсюду.
using (IEnumerator<int> enumerator = list.GetEnumerator()) {
Итак, что действительно происходит здесь.
Характер метода async
/await
делает несколько вещей для метода. В частности, весь метод поднимается на новый сгенерированный класс и превращается в конечный автомат.
Всюду, где вы видите await
, этот метод является "разделенным", так что метод должен выполняться следующим образом:
- Вызов начальной части, до первого ожидания
- Следующая часть должна обрабатываться с помощью
MoveNext
типаIEnumerator
- Следующая часть, если она есть, и все последующие части, обрабатываются этой частью
MoveNext
Этот метод MoveNext
генерируется в этом классе, а код исходного метода помещается внутри него, по частям, чтобы соответствовать различным точкам последовательности в методе.
Таким образом, любые локальные переменные метода должны выживать от одного вызова к этому методу MoveNext
до следующего, и они "поднимаются" на этот класс как частные поля.
Класс в примере может быть очень упрощенно переписан следующим образом:
public class <NotWorking>d__1
{
private int <>1__state;
// .. more things
private List<int>.Enumerator enumerator;
public void MoveNext()
{
switch (<>1__state)
{
case 0:
var list = new List<int> {1, 2, 3};
enumerator = list.GetEnumerator();
<>1__state = 1;
break;
case 1:
var dummy1 = enumerator;
Trace.WriteLine(dummy1.MoveNext());
var dummy2 = enumerator;
Trace.WriteLine(dummy2.Current);
<>1__state = 2;
break;
Этот код нигде не находится рядом с правильным кодом, но достаточно близко для этой цели.
Проблема здесь в том, что второй случай. По какой-то причине генерируемый код считывает это поле как копию, а не как ссылку на это поле. Таким образом, вызов .MoveNext()
выполняется на этой копии. Исходное значение поля остается как-is, поэтому при чтении .Current
возвращается исходное значение по умолчанию, которое в этом случае равно 0
.
Итак, посмотрим на сгенерированный ИЛ этого метода. Я выполнил оригинальный метод (только меняя Trace
на Debug
) в LINQPad, так как он имеет возможность сбрасывать генерируемый IL.
Я не буду публиковать весь код IL здесь, но пусть найдет использование перечислителя:
Здесь var enumerator = list.GetEnumerator()
:
IL_005E: ldfld UserQuery+<NotWorking>d__1.<list>5__2
IL_0063: callvirt System.Collections.Generic.List<System.Int32>.GetEnumerator
IL_0068: stfld UserQuery+<NotWorking>d__1.<enumerator>5__3
И вот вызов MoveNext
:
IL_007F: ldarg.0
IL_0080: ldfld UserQuery+<NotWorking>d__1.<enumerator>5__3
IL_0085: stloc.3 // CS$0$0001
IL_0086: ldloca.s 03 // CS$0$0001
IL_0088: call System.Collections.Generic.List<System.Int32>+Enumerator.MoveNext
IL_008D: box System.Boolean
IL_0092: call System.Diagnostics.Debug.WriteLine
ldfld
здесь читает значение поля и выталкивает значение в стеке. Затем эта копия хранится в локальной переменной метода .MoveNext()
, и эта локальная переменная затем мутируется путем вызова .MoveNext()
.
Поскольку конечный результат, теперь в этой локальной переменной, новее сохраняется в поле, поле остается как-есть.
Вот другой пример, который делает проблему более понятной в том смысле, что перечислитель, являющийся структурой, скрыт от нас:
async void Main()
{
await NotWorking();
}
public async Task NotWorking()
{
using (var evil = new EvilStruct())
{
await Task.Delay(100);
evil.Mutate();
Debug.WriteLine(evil.Value);
}
}
public struct EvilStruct : IDisposable
{
public int Value;
public void Mutate()
{
Value++;
}
public void Dispose()
{
}
}
Это тоже выдаст 0
.