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

我刚刚遇到以下行为:

for (var i = 0; i  { 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(); 

这与lambdas有关,而不是线程。 lambda捕获对变量的引用,而不是变量的值。 这意味着当您尝试在代码中使用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个盒装整数但只有一个代表

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

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

Lambda表达式不会捕获外部变量的值,而是捕获它的引用。 这就是为什么你在任务中看到50After的原因。

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

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

例:

  List acc = new List(); 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 action = () => Debug.Print("Error: " + i); for(;i<50;+i){ Task.Factory.StartNew(action); } 

另一方面,如果你希望它实际打印"Error: 1"..."Error 50"你可以改变上面的

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

第一个关闭i并将在执行Action时使用i的状态,并且状态通常是循环结束后的状态。 在后一种情况下, i被热切地评估,因为它作为参数传递给函数。 然后,此函数返回一个传递给StartNewAction

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

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

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

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