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

本文关键字:外部 变量 lambda 表达式 | 更新日期: 2023-09-27 18:37:13

我刚刚遇到了以下行为:

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

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

这更多地与 lambda 有关,而不是线程。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);
}

使用状态参数会导致较少的分配。查看反编译的代码:

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

这是因为您正在新线程中运行代码,并且主线程立即继续更改变量。如果立即执行 lambda 表达式,则使用任务的全部意义将丢失。

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

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 表达式是延迟计算的,因此在实际调用之前不会对其进行计算。在您的情况下由任务执行。如果您在 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关闭,并将使用执行操作时的i状态,并且状态通常是循环完成后的状态。在后一种情况下,i被急切地计算,因为它作为参数传递给函数。然后,此函数返回传递给 StartNewAction<int>

因此,设计决策使懒惰评估和急切评估成为可能。懒惰,因为局部变量是封闭的,并且急切地因为您可以通过将它们作为参数传递来强制执行局部变量,或者如下所示声明另一个范围较短的本地

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