为什么不Enumerator.MoveNext工作作为我希望它用和异步等待使用时? [英] Why is Enumerator.MoveNext not working as I expect it when used with using and async-await?

查看:112
本文介绍了为什么不Enumerator.MoveNext工作作为我希望它用和异步等待使用时?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想通过一个列表&LT枚举; INT> 并调用异步方法。

I would like to enumerate through a List<int> and call a async method.

如果我这样做是这样的:

If I do this in this way:

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

如果我删除使用等待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

任何人能解释这种行为对我?

Can anyone explain that behavior to me?

推荐答案

下面是简短这个问题。更详细的解释如下:

Here's the short of this problem. A longer explanation follows.


  • <$c$c>List<T>.GetEnumerator()返回一个结构,值类型。

  • 这个结构是可变的(总是灾难配方)

  • 使用(){} 是present,该结构存储在一个领域上的潜在生成的类来处理等待部分。

  • 致电时 .MoveNext()通过这个领域中,字段值的副本从底层对象加载,因此它仿佛 MoveNext的当code读 .Current
  • 从未叫
  • List<T>.GetEnumerator() returns a struct, a value type.
  • This struct is mutable (always a recipe for disaster)
  • When the using () {} is present, the struct is stored in a field on the underlying generated class to handle the await part.
  • When calling .MoveNext() through this field, a copy of the field value is loaded from the underlying object, thus it is as though MoveNext was never called when the code reads .Current

正如马克在评论中提到的,现在你知道了这个问题,一个简单的修复是重写code,明确框结构,这将确保可变的结构是到处被使用的同一个在这code,而不是新鲜的副本被突变所有的地方。

As Marc mentioned in the comments, now that you know of the problem, a simple "fix" is to rewrite the code to explicitly box the struct, this will make sure the mutable struct is the same one used everywhere in this code, instead of fresh copies being mutated all over the place.

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


那么,会发生什么情况的真正的位置。

方法的异步 / 等待性质做了几件事情的方法。具体而言,整个方法提升到一个新生成的类,变成了一个状态机。

The async / await nature of a method does a few things to a method. Specifically, the entire method is lifted onto a new generated class and turned into a state machine.

到处可以看到等待,该方法是一种分裂的,这样的方法是像这样的执行:

Everywhere you see await, the method is sort of "split" so that the method has to be executed sort of like this:


  1. 通话起始部分,直到第一的await

  2. 接下来的部分必须由被处理的的MoveNext 有点像一个的IEnumerator

  3. 接下来的部分,如果有的话,所有的后续部分,全部由该处理的MoveNext 部分

  1. Call initial part, up until the first await
  2. The next part will have to be handled by a MoveNext sort of like an IEnumerator
  3. The next part, if any, and all subsequent parts, are all handled by this MoveNext part

在这个类是产生该的MoveNext 方法,并从原来的方法code放在里面,零碎的,以适应各种sequencepoints的方法。

This MoveNext method is generated on this class, and the code from the original method is placed inside it, piecemeal to fit the various sequencepoints in the method.

因此​​,任何的本地的方法变量总是要生存的一个电话本的MoveNext 方法下,他们是解禁到这个类的私人领域。

As such, any local variables of the method has to survive from one call to this MoveNext method to the next, and they are "lifted" onto this class as private fields.

在示例中的类可以再的非常简单地的改写为这样的:

The class in the example can then very simplistically be rewritten to something like this:

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;

这code是的远不正确的code 的,但足够接近达到这一目的。

This code is nowhere near the correct code, but close enough for this purpose.

这里的问题是,第二壳体。为code产生某种原因读取该字段作为一个副本,而不是作为对场的参考。因此,调用 .MoveNext()是在这个副本完成。原来的字段值保持原样,因此当 .Current 被读取时,原始的默认值返回,在这种情况下是 0

The problem here is that second case. For some reason the code generated reads this field as a copy, and not as a reference to the field. As such, the call to .MoveNext() is done on this copy. The original field value is left as-is, so when .Current is read, the original default value is returned, which in this case is 0.

因此​​,让我们来看看这个方法的产生IL。我的跟踪到调试):// linqpad。净> LINQPad 因为它以转储IL产生的能力。

So let's look at the generated IL of this method. I executed the original method (only changing Trace to Debug) in LINQPad since it has the ability to dump the IL generated.

我不会在这里张贴整个IL code,但让我们发现枚举器的用法:

I won't post the whole IL code here, but let's find the usage of the enumerator:

下面的 VAR枚举= 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

<一个href=\"https://msdn.microsoft.com/en-us/library/system.reflection.emit.op$c$cs.ldfld%28v=vs.110%29.aspx\"><$c$c>ldfld这里读取字段值和推栈上的值。然后,这个副本存储在 .MoveNext()方法的局部变量,然后这个局部变量是通过对 .MoveNext通话(突变)

ldfld here reads the field value and pushes the value on the stack. Then this copy is stored in a local variable of the .MoveNext() method, and this local variable is then mutated through a call to .MoveNext().

由于最终的结果,现在在这个局部变量,是较新的存储回领域,该领域是保持原样。

Since the end result, now in this local variable, is newer stored back into the field, the field is left as-is.

下面是一个不同的例子,这使得这个问题更清晰在这个意义上枚举是一个结构是那种从我们这里隐藏的:

Here is a different example which makes the problem "clearer" in the sense that the enumerator being a struct is sort of hidden from us:

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

这篇关于为什么不Enumerator.MoveNext工作作为我希望它用和异步等待使用时?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆