我想通过List<int>
枚举并调用异步方法。
如果我这样做:
public async Task NotWorking() {
var list = new List<int> {1, 2, 3};
using (var enumerator = list.GetEnumerator()) {
Trace.WriteLine(enumerator.MoveNext());
Trace.WriteLine(enumerator.Current);
await Task.Delay(100);
}
}
结果是:
True
0
但我希望它是:
True
1
如果我删除了using
或await Task.Delay(100)
:
public void Working1() {
var list = new List<int> {1, 2, 3};
using (var enumerator = list.GetEnumerator()) {
Trace.WriteLine(enumerator.MoveNext());
Trace.WriteLine(enumerator.Current);
}
}
public async Task Working2() {
var list = new List<int> {1, 2, 3};
var enumerator = list.GetEnumerator();
Trace.WriteLine(enumerator.MoveNext());
Trace.WriteLine(enumerator.Current);
await Task.Delay(100);
}
输出符合预期:
True
1
有人可以向我解释这种行为吗?
答案 0 :(得分:16)
这里没有这个问题。接下来是一个更长的解释。
List<T>.GetEnumerator()
返回一个结构,一个值类型。using () {}
存在时,结构存储在底层生成的类的字段中以处理await
部分。.MoveNext()
时,将从基础对象加载字段值的副本,因此,当代码读取MoveNext
<时,就好像.Current
一样/ LI>
正如Marc在评论中提到的那样,现在您已经知道了这个问题,一个简单的&#34;修复&#34;是重写代码以显式地封装结构,这将确保可变结构与本代码中的任何地方使用的结构相同,而不是在整个地方突变的新副本。
using (IEnumerator<int> enumerator = list.GetEnumerator()) {
那么,真的会发生什么。
方法的async
/ await
性质对方法做了一些事情。具体来说,整个方法被提升到一个新生成的类并转换为状态机。
在您看到await
的任何地方,该方法都是&#34; split&#34;所以必须执行这样的方法:
MoveNext
处理IEnumerator
MoveNext
部分处理在此类上生成此MoveNext
方法,并将原始方法中的代码放在其中,零碎以适合方法中的各种序列点。
因此,该方法的任何本地变量必须在从一个MoveNext
方法的调用到下一个方法中存活,并且它们被提升并且#34;作为私人领域进入这个班级。
然后,示例中的类可以非常简单地重写为以下内容:
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。我在LINQPad中执行了原始方法(仅将Trace
更改为Debug
),因为它能够转储生成的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()
来变异此局部变量。
由于最终结果(现在在此局部变量中)更新存储回字段,因此该字段保持原样。
这是一个不同的例子,可以解决问题&#34;更清晰&#34;从某种意义上说,作为结构的枚举器对我们来说是隐藏的:
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
。
答案 1 :(得分:3)
看起来像旧编译器中的一个错误,可能是由于在使用和异步中执行的代码转换的一些干扰造成的。
使用VS2015进行编译器运输似乎正确。