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();