为什么在使用异步方法时捕获类范围变量,而在使用Action <T>(内部代码示例)时不捕获?

Kru*_*lur 15 .net c# asynchronous task

虽然走路我在想狗Action<T>,Func<T>,Task<T>,async/await(是的,书呆子,我知道...),并在我的脑海里构建一个小的测试程序,并想知道答案是什么.我注意到我不确定结果,所以我创建了两个简单的测试.

这是设置:

  • 我有一个类范围变量(字符串).
  • 它被赋予初始值.
  • 变量作为参数传递给类方法.
  • 该方法不会直接执行,而是分配给"Action".
  • 在执行操作之前,我更改了变量的值.

输出会是什么?初始值,还是更改后的值?

有点令人惊讶但可以理解,输出是改变的值.我的解释:在动作执行之前,变量不会被压入堆栈,因此它将是已更改的变量.

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/c20cb3d3b4c44134311fhttps://gist.github.com/Krumelur/3f93afb50b02fba6a7c8

Ant*_*t P 7

你在这里有一些误解.首先,当你调用时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)

  • 实例化任务在`async`方法中具有相同的行为.`async`和非'async`方法之间存在一些差异,但这不是其中之一.如果在非`async`方法中调用`Task.Delay`,它仍然会立即开始执行. (2认同)

Lua*_*aan 5

在这两种情况下,你都在关闭.但是,在这两种情况下,你要对不同的东西进行封闭.

在第一种情况下,你正在创建一个带有闭包的匿名方法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)立即运行.我的答案中的其他要点也很重要,但这是关键.


Yuv*_*kov 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);
}
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任务会存储您较旧的陈旧值.这就是为什么你会看到两种不同的行为.