这是这个问题的不足之处。下面是更长的解释。
-
List<T>.GetEnumerator() https://msdn.microsoft.com/en-us/library/b0yss765(v=vs.110).aspx返回一个结构体,一个值类型。
- 这个结构是可变的(永远是灾难的根源 https://stackoverflow.com/questions/441309/why-are-mutable-structs-evil)
- 当。。。的时候
using () {}
存在时,该结构存储在底层生成类的字段中以处理await
part.
- 打电话时
.MoveNext()
通过这个字段,从底层对象加载字段值的副本,因此就好像MoveNext
读取代码时从未被调用.Current
正如 Marc 在评论中提到的,既然您知道了问题,一个简单的“修复”就是重写代码以显式装箱结构,这将确保可变结构与此代码中各处使用的结构相同,而不是新的副本到处都在变异。
using (IEnumerator<int> enumerator = list.GetEnumerator()) {
那么,会发生什么really here.
The async
/ await
方法的本质对方法做了一些事情。具体来说,整个方法被提升到一个新生成的类上并变成一个状态机。
随处可见await
,该方法有点“拆分”,因此该方法必须像这样执行:
- 调用初始部分,直到第一个等待
- 下一部分必须由
MoveNext
有点像IEnumerator
- 下一部分(如果有的话)以及所有后续部分都由这个处理
MoveNext
part
This MoveNext
方法是在此类上生成的,原始方法中的代码被零碎地放置在其中,以适应方法中的各个序列点。
因此,任何local该方法的变量必须从对此的一次调用中幸存下来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()
在此副本上完成。原始字段值保持原样,所以当.Current
读取时,返回原始默认值,在本例中为0
.
那么我们来看看这个方法生成的IL。我执行了原来的方法(只改变Trace
to Debug
) in LINQPad http://linqpad.net因为它能够转储生成的 IL。
我不会在这里发布完整的 IL 代码,但让我们看看枚举器的用法:
Here's 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 https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldfld%28v=vs.110%29.aspx这里读取字段值并将该值压入堆栈。然后这个副本被存储在一个局部变量中.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
.