Kru*_*lur 15 .net c# asynchronous task
虽然走路我在想狗Action<T>,Func<T>,Task<T>,async/await(是的,书呆子,我知道...),并在我的脑海里构建一个小的测试程序,并想知道答案是什么.我注意到我不确定结果,所以我创建了两个简单的测试.
这是设置:
输出会是什么?初始值,还是更改后的值?
有点令人惊讶但可以理解,输出是改变的值.我的解释:在动作执行之前,变量不会被压入堆栈,因此它将是已更改的变量.
public class foo
{
string token;
public foo ()
{
this.token = "Initial Value";
}
void DoIt(string someString)
{
Console.WriteLine("SomeString is '{0}'", someString);
}
public void Run()
{
Action op = () => DoIt(this.token);
this.token = "Changed value";
// Will output "Changed value".
op();
}
}
Run Code Online (Sandbox Code Playgroud)
接下来,我创建了一个变体:
public class foo
{
string token;
public foo ()
{
this.token = "Initial Value";
}
Task DoIt(string someString)
{
// Delay(0) is just there to try if timing is the issue here - can also try Delay(1000) or whatever.
return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", someString));
}
async Task Execute(Func<Task> op)
{
await op();
}
public async void Run()
{
var op = DoIt(this.token);
this.token = "Changed value";
// The output will be "Initial Value"!
await Execute(() => op);
}
}
Run Code Online (Sandbox Code Playgroud)
我在这里DoIt()回来了Task.op现在是一个Task,不再是一个Action.该Execute()方法等待该任务.令我惊讶的是,输出现在是"初始值".
为什么它的表现不同?
DoIt()在Execute()被调用之前不会被执行,那为什么它会捕获初始值token?
完整测试:https://gist.github.com/Krumelur/c20cb3d3b4c44134311f和https://gist.github.com/Krumelur/3f93afb50b02fba6a7c8
你在这里有一些误解.首先,当你调用时DoIt,它返回一个已经开始执行的Task.只有在您执行任务时才会启动执行await.
您还可以在someString变量上创建一个闭包,当您重新分配类级别字段时,该变量的值不会更改:
Task DoIt(string someString)
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", someString));
}
Run Code Online (Sandbox Code Playgroud)
在Action传递给ContinueWith在关闭someString变量.请记住,字符串是不可变的,因此,当您重新分配值时token,实际上是在分配新的字符串引用.但是,someString里面的局部变量DoIt保留旧引用,因此即使在重新分配类字段后,其值仍保持不变.
您可以通过直接在类级别字段上关闭此操作来解决此问题:
Task DoIt()
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", this.token));
}
Run Code Online (Sandbox Code Playgroud)
在这两种情况下,你都在关闭.但是,在这两种情况下,你要对不同的东西进行封闭.
在第一种情况下,你正在创建一个带有闭包的匿名方法this- 当你最终执行委托时,它将获取当前值this,获取当前值this.token并使用它.所以你看到修改后的值.
在第二种情况下,没有关闭this- 或者如果是,它没有任何区别.你this.token明确地传递,并且该DoIt方法只需要对它自己的参数进行闭包,someString.这种情况立即(同步)发生,而不是懒惰地发生 - 因此this.token捕获了初始值.await实际上并不执行委托 - 它只等待异步方法的结果.该方法本身已经运行,只有它的异步部分是异步的 - 在这种情况下,只有Console.WriteLine("SomeString is '{0}'", someString).
如果你想更清楚地看到这一点,那么添加Thread.Sleep(1000)之后this.token = "Changed value";- 你会看到在你到达之前SomeString is 'Initial Value'打印出来.await
为了使第二个示例与第一个示例相同,您需要做的就是op再次更改为委托,而不是Task- var op = () => DoIt(this.token);.这会DoIt再次延迟执行,并导致与第一个示例中相同的闭包.
TL; DR:
行为是不同的,因为在第一种情况下,您推迟执行DoIt(this.token),而在第二个示例中,您DoIt(this.token)立即运行.我的答案中的其他要点也很重要,但这是关键.
让我们分解每个案例.
从以下开始Action<T>:
我的解释:在动作执行之前,变量不会被压入堆栈,因此它将是已更改的变量
这与堆栈无关.编译器从第一个代码段生成以下内容:
public foo()
{
this.token = "Initial Value";
}
private void DoIt(string someString)
{
Console.WriteLine("SomeString is '{0}'", someString);
}
public void Run()
{
Action action = new Action(this.<Run>b__3_0);
this.token = "Changed value";
action();
}
[CompilerGenerated]
private void <Run>b__3_0()
{
this.DoIt(this.token);
}
Run Code Online (Sandbox Code Playgroud)
编译器从lambda表达式发出一个命名方法.一旦你调用了这个动作,并且因为我们在同一个类中,那this.token就是更新的"Changed Value".编译器甚至不会将其提升为显示类,因为这是在实例方法中创建和调用的.
现在,为async方法.有两个状态机正在生成,不利于国家机器的膨胀,并进入相关部分.状态机执行以下操作:
this.<>8__1 = new foo.<>c__DisplayClass4_0();
this.<>8__1.op = this.<>4__this.DoIt(this.<>4__this.token);
this.<>4__this.token = "Changed value";
taskAwaiter = this.<>4__this.Execute(new Func<Task>(this.<>8__1.<Run>b__0)).GetAwaiter();
Run Code Online (Sandbox Code Playgroud)
这里发生了什么?token被传递给DoIt,将返回一个Func<Task>.该委托包含对旧标记字符串"Initial Value"的引用.请记住,即使我们谈论的是引用类型,它们都是通过值传递的.这实际上意味着现在在DoIt方法中存在指向"初始值" 的旧字符串的新存储位置.然后,下一行更改token为"更改值".该string存储内Func和已更改一个是现在在两个不同的字符串指向.
当您调用委托时,它将打印初始值,因为op任务会存储您较旧的陈旧值.这就是为什么你会看到两种不同的行为.
| 归档时间: |
|
| 查看次数: |
1474 次 |
| 最近记录: |