为什么Enumerator.MoveNext与using和async-await一起使用时无法正常工作?

时间:2015-03-24 10:50:20

标签: c# list async-await ienumerable

我想通过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

如果我删除了usingawait 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

有人可以向我解释这种行为吗?

2 个答案:

答案 0 :(得分:16)

这里没有这个问题。接下来是一个更长的解释。

  • List<T>.GetEnumerator()返回一个结构,一个值类型。
  • 此结构是可变的(always a recipe for disaster
  • using () {}存在时,结构存储在底层生成的类的字段中以处理await部分。
  • 通过此字段调用.MoveNext()时,将从基础对象加载字段值的副本,因此,当代码读取MoveNext <时,就好像.Current一样/ LI>

正如Marc在评论中提到的那样,现在您已经知道了这个问题,一个简单的&#34;修复&#34;是重写代码以显式地封装结构,这将确保可变结构与本代码中的任何地方使用的结构相同,而不是在整个地方突变的新副本。

using (IEnumerator<int> enumerator = list.GetEnumerator()) {

那么,真的会发生什么。

方法的async / await性质对方法做了一些事情。具体来说,整个方法被提升到一个新生成的类并转换为状态机。

在您看到await的任何地方,该方法都是&#34; split&#34;所以必须执行这样的方法:

  1. 调用初始部分,直至第一个等待
  2. 下一部分必须由MoveNext处理IEnumerator
  3. 下一部分(如果有)以及所有后续部分均由此MoveNext部分处理
  4. 在此类上生成此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进行编译器运输似乎正确。