如何在lambda表达式中捕获外部变量的值?

时间:2012-06-15 10:53:44

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

我刚刚遇到以下行为:

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

4 个答案:

答案 0 :(得分:26)

这与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());
    });
}

最佳解决方案是将loop参数作为状态参数传递:

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

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

  • 第二个代码段将创建50个闭包和50个代表
  • 第三个代码段将创建50个盒装整数但只有一个代表

答案 1 :(得分:4)

那是因为您在新线程中运行代码,并且主线程立即继续更改变量。如果lambda表达式立即执行,则使用任务的整个过程都将丢失。

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

答案 2 :(得分: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());

答案 3 :(得分:1)

根据定义,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<int>,并传递给StartNew

因此,设计决策使懒惰的评估和急切的评估成为可能。懒惰,因为你可以通过将它们作为参数传递来强制执行本地人,或者如下所示声明另一个具有较短范围的本地,因此迫切需要本地人关闭

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

以上所有内容都适用于Lambdas。在StartNew的特定情况下,实际上有一个重载可以执行第二个示例所做的事情,可以简化为

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