Pet*_*ger 22 c# ienumerable list async-await
我想通过a枚举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);
}
}
Run Code Online (Sandbox Code Playgroud)
结果是:
True
0
Run Code Online (Sandbox Code Playgroud)
但我希望它是:
True
1
Run Code Online (Sandbox Code Playgroud)
如果我删除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);
}
Run Code Online (Sandbox Code Playgroud)
输出符合预期:
True
1
Run Code Online (Sandbox Code Playgroud)
任何人都可以向我解释这种行为吗?
ang*_*son 17
这是这个问题的不足之处.更长的解释如下.
List<T>.GetEnumerator() 返回一个结构,一个值类型.using () {}存在时,结构存储在底层生成的类的字段中以处理该await部分..MoveNext()通过这个字段调用时,从底层对象加载一个字段值的副本,因此就好像MoveNext在代码读取时从未调用过.Current正如Marc在评论中提到的,现在您已经知道了这个问题,一个简单的"修复"是重写代码以显式地封装结构,这将确保可变结构与此代码中的任何地方使用的结构相同,而不是新鲜的副本在各处变异.
using (IEnumerator<int> enumerator = list.GetEnumerator()) {
Run Code Online (Sandbox Code Playgroud)
那么,这里真正发生了什么.
方法的async/ await性质对方法做了一些事情.具体来说,整个方法被提升到新生成的类并转换为状态机.
你看到的任何地方await,这个方法都是"拆分"的,所以方法必须像这样执行:
MoveNext某种方式来处理IEnumeratorMoveNext部分处理此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;
Run Code Online (Sandbox Code Playgroud)
此代码远不是正确的代码,但足够接近此目的.
这里的问题是第二种情况.由于某种原因,生成的代码将此字段作为副本读取,而不是作为对该字段的引用.因此,调用.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
Run Code Online (Sandbox Code Playgroud)
以下是对以下内容的呼吁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
Run Code Online (Sandbox Code Playgroud)
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()
{
}
}
Run Code Online (Sandbox Code Playgroud)
这也将输出0.
| 归档时间: |
|
| 查看次数: |
1666 次 |
| 最近记录: |