在遛狗时我正在考虑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();
}
}
接下来,我创建了一个变体:
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);
}
}
我在此DoIt()
返回Task
。 op
现在是Task
,不再是Action
。 Execute()
方法等待任务。令我惊讶的是,输出现在是&#34;初始值&#34;。
为什么表现不同?
DoIt()
在Execute()
被调用之前不会被执行,那么为什么它会捕获token
的初始值?
完成测试:https://gist.github.com/Krumelur/c20cb3d3b4c44134311f和https://gist.github.com/Krumelur/3f93afb50b02fba6a7c8
答案 0 :(得分:7)
你有一些误解。首先,当您调用DoIt
时,它将返回已经开始执行的任务。执行不会仅在您await
任务时启动。
当您重新分配类级字段时,还会在someString
变量上创建一个闭包,的值不会改变:
Task DoIt(string someString)
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", someString));
}
传递给Action
的{{1}}在ContinueWith
变量上关闭。请记住,字符串是不可变的,因此,当您重新分配someString
的值时,实际上是在分配新字符串引用。但是,token
内的局部变量someString
会保留旧引用,因此即使在重新分配类字段后,其值仍保持不变。
您可以通过直接在类级字段上关闭此操作来解决此问题:
DoIt
答案 1 :(得分:5)
在这两种情况下,你都在关闭。但是,在这两种情况下,你要对不同的东西进行封闭。
在第一种情况下,您使用this
上的闭包创建一个匿名方法 - 当您最终执行委托时,它将采用{em>当前值{{ 1}},获取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'
1}}。
要使第二个示例与第一个示例相同,您需要做的就是将await
再次更改为委托,而不是op
- Task
。这会再次延迟var op = () => DoIt(this.token);
的执行,并导致与第一个示例中相同的闭包。
TL; DR:
行为不同,因为在第一种情况下,您推迟执行DoIt
,而在第二种情况下,您立即运行DoIt(this.token)
。我的答案中的其他要点也很重要,但这是关键。
答案 2 :(得分:5)
让我们分解每一个案例。
从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);
}
编译器从lambda表达式发出一个命名方法。一旦您调用该操作,并且由于我们在同一个类中,this.token
是更新的“更改值”。编译器甚至不会将其提升为显示类,因为这是在实例方法中创建和调用的。
现在,对于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();
这里发生了什么? token
会传递给DoIt
,Func<Task>
将返回DoIt
。该委托包含对旧标记字符串“Initial Value”的引用。请记住,即使我们谈论的是引用类型,它们都是通过值传递的。这实际上意味着现在token
方法中存在旧字符串的新存储位置,该位置指向“初始值”。然后,下一行将string
更改为“已更改的值”。存储在Func
内的op
和已更改的evidensapp_polystructures
现在指向两个不同的字符串。
当您调用委托时,它将打印初始值,因为evidensapp_seniangcbr
任务会存储您较旧的陈旧值。这就是为什么你会看到两种不同的行为。