C# 为什么在使用异步方法时捕获类作用域变量,而在使用操作时不捕获<;T>;(里面的代码示例)?
遛狗的时候,我在想C# 为什么在使用异步方法时捕获类作用域变量,而在使用操作时不捕获<;T>;(里面的代码示例)?,c#,.net,asynchronous,task,C#,.net,Asynchronous,Task,遛狗的时候,我在想动作,函数,任务,异步/等待(是的,书呆子,我知道…),然后在脑海里构建了一个小测试程序,想知道答案是什么。我注意到我对结果不确定,所以我创建了两个简单的测试 以下是设置: 我有一个类范围变量(字符串) 它被分配一个初始值 变量作为参数传递给类方法 该方法不会直接执行,而是分配给“操作” 在执行操作之前,我更改变量的值 输出是什么?初始值还是更改后的值? 有点令人惊讶,但可以理解,输出是更改后的值。我的解释是:在操作执行之前,变量不会被推送到堆栈上,因此它将是更改后的变量
动作
,函数
,任务
,异步/等待
(是的,书呆子,我知道…),然后在脑海里构建了一个小测试程序,想知道答案是什么。我注意到我对结果不确定,所以我创建了两个简单的测试
以下是设置:
- 我有一个类范围变量(字符串)
- 它被分配一个初始值
- 变量作为参数传递给类方法
- 该方法不会直接执行,而是分配给“操作”
- 在执行操作之前,我更改变量的值
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();
}
}
接下来,我创建了一个变体:
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);
}
}
公共类foo
{
字符串标记;
公共食品()
{
this.token=“初始值”;
}
任务DoIt(字符串someString)
{
//延迟(0)只是在这里尝试,如果这里的时间是问题-也可以尝试延迟(1000)或任何东西。
return Task.Delay(0).ContinueWith(t=>Console.WriteLine(“SomeString是{0}',SomeString));
}
异步任务执行(Func op)
{
等待op();
}
公共异步无效运行()
{
var op=DoIt(this.token);
this.token=“更改的值”;
//输出将是“初始值”!
等待执行(()=>op);
}
}
在这里,我让DoIt()
返回一个任务
<代码>操作现在是一项任务
,不再是操作
。Execute()
方法等待任务。令我惊讶的是,输出现在是“初始值”
为什么它的行为会有所不同?
DoIt()
在调用Execute()
之前不会执行,那么为什么它会捕获令牌的初始值呢
完成测试:并且在这两种情况下,您都在进行关闭。然而,在这两种情况下,你正在对不同的事情进行总结
在第一种情况下,您正在创建一个匿名方法,在this
上使用闭包-当您最终执行委托时,它将获取this
的当前值,获取this.token
的当前值并使用该值。因此,您可以看到修改后的值
在第二种情况下,这个
没有闭包,或者如果是闭包,也没有什么区别。您显式地传递this.token
,而DoIt
方法只需要对自己的参数someString
进行闭包。这会立即(同步)发生,而不是延迟发生-因此捕获This.token
的初始值wait
实际上并不执行委托-它只等待异步方法的结果。该方法本身已经运行,并且只有它的异步部分是异步的——在本例中,只有控制台.WriteLine(“SomeString是{0}',SomeString)
如果您想更清楚地看到这一点,请在this.token=“Changed value”之后添加Thread.Sleep(1000)
代码>-在您到达等待
之前,您将看到打印出来的字符串是“初始值”
要使第二个示例的行为与第一个示例类似,只需将op
再次更改为委托,而不是Task
-var op=()=>DoIt(this.token)代码>。这会再次延迟执行DoIt
,并导致与第一个示例中相同的关闭
TL;博士:
这种行为是不同的,因为在第一种情况下,您延迟执行DoIt(this.token)
,而在第二个示例中,您立即运行DoIt(this.token)
。我的回答中的其他几点也很重要,但这是关键。这里有一些误解。首先,当您调用DoIt
时,它返回一个已经开始执行的任务。执行不会仅在您等待任务时开始
您还可以在someString
变量上创建闭包,当您重新指定class level字段时,该变量的值不会更改:
Task DoIt(string someString)
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", someString));
}
传递给ContinueWith
的操作将关闭someString
变量。请记住,字符串是不可变的,因此,当您重新分配标记的值时,实际上是在分配一个新的字符串引用。但是,DoIt
内部的局部变量someString
保留了旧的引用,因此即使在重新分配类字段后,其值也保持不变
您可以通过直接在类级别字段上关闭此操作来解决此问题:
Task DoIt()
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", this.token));
}
让我们把每一个案例都分解一下
从操作开始
:
我的解释是:直到
动作执行,因此它将是更改的动作
这与堆栈无关。编译器从您的第一个代码段生成以下内容:
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);
}
这里发生了什么<代码>标记
被传递到DoIt
,它将返回一个Func
。该委托包含对旧令牌字符串“初始值”的引用。记住,即使我们讨论的是引用类型,它们都是通过值传递的。这实际上意味着在DoIt
方法中有一个指向“初始值”的旧字符串的新存储位置。然后,下一行将标记更改为“更改的值”。存储在Func
中的字符串
和已更改的字符串
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();