【问题标题】:How can I capture the value of an outer variable inside a lambda expression?如何在 lambda 表达式中捕获外部变量的值?
【发布时间】:2012-06-18 10:37:28
【问题描述】:

我刚刚遇到以下行为:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i.ToString());
    });
}

将导致一系列“错误:x”,其中大部分 x 等于 50。

同样:

var a = "Before";
var task = new Task(() => Debug.Print("Using value: " + a));
a = "After";
task.Start();

将导致“使用值:之后”。

这显然意味着 lambda 表达式中的连接不会立即发生。在声明表达式时,如何在 lambda 表达式中使用外部变量的副本?以下不会更好(我承认这不一定是不连贯的):

var a = "Before";
var task = new Task(() => {
    var a2 = a;
    Debug.Print("Using value: " + a2);
});
a = "After";
task.Start();

【问题讨论】:

  • 他们为什么要这样做?无论如何,它们都是异步的。
  • 可能重复“循环中的 C# 捕获变量”stackoverflow.com/questions/271440/…
  • 恕我直言,你最终在这里问了 2 个问题——“真正的”问题似乎在标题中(如何捕获值,以便任务在循环时在值上运行),但是那么问题的主体似乎集中在“为什么这些事情会导致意外的值”(闭包捕获的效果意味着它们都引用了同一个变量)。因此,您最终会得到大多数解释行为的答案,而不是回答您的“真实”问题(AFAICT :)
  • True J​​ames,实际上,在阅读了最初的 cmets 之后,我更改了我的问题标题以更好地反映我的观点。

标签: c# asynchronous lambda closures task-parallel-library


【解决方案1】:

这更多地与 lambdas 相关,而不是线程。 lambda 捕获对变量的引用,而不是变量的值。这意味着当您尝试在代码中使用 i 时,它的值将是最后存储在 i 中的值。

为避免这种情况,您应该在 lambda 启动时将变量的值复制到局部变量。问题是,启动任务有开销,并且第一个副本只能在循环完成后执行。 以下代码也会失败

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        var i1=i;
        Debug.Print("Error: " + i1.ToString());
    });
}

正如 James Manning 所说,您可以在循环中添加一个局部变量并将循环变量复制到那里。这样你就创建了 50 个不同的变量来保存循环变量的值,但至少你得到了预期的结果。问题是,你确实得到了很多额外的分配。

for (var i = 0; i < 50; ++i) {
    var i1=i;
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i1.ToString());
    });
}

最好的解决方案是将循环参数作为状态参数传递:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(o => {
        var i1=(int)o;
        Debug.Print("Error: " + i1.ToString());
    }, i);
}

使用状态参数可以减少分配。查看反编译后的代码:

  • 第二个 sn-p 将创建 50 个闭包和 50 个委托
  • 第三个 sn-p 将创建 50 个盒装整数,但只有一个委托

【讨论】:

  • 情况已经很清楚了,这里描述一下:blogs.msdn.com/b/ericlippert/archive/2009/11/12/…
  • 对于第一个循环,“正确”修复 (AFAICT) 是执行 var i1 = i;在循环内但在 Task.Factory.StartNew 之前。通过这种更改,每个闭包都将引用其自己的单独变量,您将获得正确的效果。 state 参数避免了对闭包的需要,因此当然更有效,但如果您只想要正确的行为,则不是必需的。
  • 并不是说它不起作用(这就是语言的工作方式),而是 lambdas 可能只有在循环完成后才开始执行
  • @James Manning,你是对的,这只会在循环中创建一个局部变量,因此不可能捕获错误的变量
  • @PanagiotisKanavos - 根据 Erwin 的评论,如果您更改第一个代码块以进行更改,听起来他会接受它作为答案。
【解决方案2】:

那是因为你在一个新线程中运行代码,而主线程立即继续更改变量。如果立即执行 lambda 表达式,则使用任务的全部意义都将丢失。

线程在创建任务时并没有获得自己的变量副本,所有任务都使用同一个变量(实际上存储在方法的闭包中,它不是局部变量)。

【讨论】:

    【解决方案3】:

    Lambda 表达式捕获的不是外部变量的值,而是对其的引用。这就是您在任务中看到50After 的原因。

    要解决此问题,请在 lambda 表达式之前创建它的副本以按值捕获它。

    这种不幸的行为将由 .NET 4.5 的 C# 编译器修复,直到那时你需要忍受这种奇怪。

    例子:

        List<Action> acc = new List<Action>();
        for (int i = 0; i < 10; i++)
        {
            int tmp = i;
            acc.Add(() => { Console.WriteLine(tmp); });
        }
    
        acc.ForEach(x => x());
    

    【讨论】:

    • 你的意思是在 lambda 表达式中创建一个副本会起作用吗?目前它没有:使用 var a2 = a; Logging.Print("使用值:" + a2);仍然返回“使用值:之后”。
    • 对不起。您需要将副本放在 lambda 之外以使其工作。
    【解决方案4】:

    根据定义,Lambda 表达式是惰性求值的,因此它们在实际调用之前不会被求值。在您的情况下,由任务执行。如果您在 lambda 表达式中关闭本地,则将反映执行时本地的状态。这就是你所看到的。您可以利用这一点。例如。你的 for 循环真的不需要每次迭代都需要一个新的 lambda,假设为了示例所描述的结果是你想要写的结果

    var i =0;
    Action<int> action = () => Debug.Print("Error: " + i);
    for(;i<50;+i){
        Task.Factory.StartNew(action);
    }
    

    另一方面,如果您希望它实际打印"Error: 1"..."Error 50",您可以将上面的内容更改为

    var i =0;
    Func<Action<int>> action = (x) => { return () => Debug.Print("Error: " + x);}
    for(;i<50;+i){
        Task.Factory.StartNew(action(i));
    }
    

    第一个关闭i,并在执行Action 时使用i 的状态,并且该状态通常是循环结束后的状态。在后一种情况下,i 被急切地评估,因为它作为参数传递给函数。然后这个函数返回一个Action&lt;int&gt;,它被传递给StartNew

    因此,设计决策使惰性评估和急切评估都成为可能。懒惰是因为本地人被关闭并且急切地因为您可以通过将本地人作为参数传递或如下所示声明另一个具有更短范围的本地人来强制执行本地人

    for (var i = 0; i < 50; ++i) {
        var j = i;
        Task.Factory.StartNew(() => Debug.Print("Error: " + j));
    }
    

    以上所有内容对于 Lambda 都是通用的。在 StartNew 的特定情况下,实际上有一个重载,它执行第二个示例的操作,因此可以简化为

    var i =0;
    Action<object> action = (x) => Debug.Print("Error: " + x);}
    for(;i<50;+i){
        Task.Factory.StartNew(action,i);
    }
    

    【讨论】:

    • 顺便说一句,那个 lambda 可以简化为 x =&gt; () =&gt; Debug.Print("Error: " + x)
    猜你喜欢
    • 2015-11-23
    • 2015-08-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多