C# 如何在lambda表达式中捕获外部变量的值?
我刚刚遇到了以下行为:C# 如何在lambda表达式中捕获外部变量的值?,c#,asynchronous,lambda,closures,task-parallel-library,C#,Asynchronous,Lambda,Closures,Task Parallel Library,我刚刚遇到了以下行为: for (var i = 0; i < 50; ++i) { Task.Factory.StartNew(() => { Debug.Print("Error: " + i.ToString()); }); } 将导致“使用值:之后” 这显然意味着lambda表达式中的串联不会立即发生。在声明表达式时,如何在lambda表达式中使用外部变量的副本?以下几点效果不会更好(我承认这并不一定是不连贯的): 这是因为您正在一个新线程中
for (var i = 0; i < 50; ++i) {
Task.Factory.StartNew(() => {
Debug.Print("Error: " + i.ToString());
});
}
将导致“使用值:之后”
这显然意味着lambda表达式中的串联不会立即发生。在声明表达式时,如何在lambda表达式中使用外部变量的副本?以下几点效果不会更好(我承认这并不一定是不连贯的):
这是因为您正在一个新线程中运行代码,并且主线程立即继续更改变量。如果立即执行lambda表达式,则使用任务的整个点都将丢失
创建任务时,线程没有获取自己的变量副本,所有任务都使用同一个变量(实际上存储在方法的闭包中,它不是局部变量)。Lambda表达式不捕获外部变量的值,而是捕获外部变量的引用。这就是为什么您在任务中看到
50
或之后的原因
要解决此问题,请在lambda表达式之前创建它的副本以按值捕获它
这种不幸的行为将由.NET4.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());
List acc=new List();
对于(int i=0;i<10;i++)
{
int-tmp=i;
acc.Add(()=>{Console.WriteLine(tmp);});
}
acc.ForEach(x=>x());
这与lambda的关系大于线程。lambda捕获对变量的引用,而不是变量的值。这意味着当您尝试在代码中使用i时,它的值将是上次存储在i中的值
为了避免这种情况,应该在lambda启动时将变量的值复制到局部变量。问题是,启动任务有开销,只有在循环完成后才能执行第一个副本以下代码也将失败
for (var i = 0; i < 50; ++i) {
Task.Factory.StartNew(() => {
var i1=i;
Debug.Print("Error: " + i1.ToString());
});
}
for(变量i=0;i<50;++i){
Task.Factory.StartNew(()=>{
var i1=i;
Debug.Print(“错误:+i1.ToString());
});
}
正如James Manning所指出的,您可以将一个局部变量添加到循环中,然后将循环变量复制到那里。这样,您就创建了50个不同的变量来保存循环变量的值,但至少您得到了预期的结果。问题是,你确实得到了很多额外的分配
for (var i = 0; i < 50; ++i) {
var i1=i;
Task.Factory.StartNew(() => {
Debug.Print("Error: " + i1.ToString());
});
}
for(变量i=0;i<50;++i){
var i1=i;
Task.Factory.StartNew(()=>{
Debug.Print(“错误:+i1.ToString());
});
}
最佳解决方案是将循环参数作为状态参数传递:
for (var i = 0; i < 50; ++i) {
Task.Factory.StartNew(o => {
var i1=(int)o;
Debug.Print("Error: " + i1.ToString());
}, i);
}
for(变量i=0;i<50;++i){
Task.Factory.StartNew(o=>{
VarI1=(int)o;
Debug.Print(“错误:+i1.ToString());
},i);
}
使用状态参数会减少分配。查看反编译代码:
- 第二个代码段将创建50个闭包和50个委托
- 第三个代码段将创建50个装箱整数,但只创建一个委托
根据定义,Lambda表达式是延迟计算的,因此在实际调用之前不会计算它们。在您的情况下,由任务执行。如果在lambda表达式中关闭局部,则将反映执行时局部的状态。这就是你看到的。你可以利用这一点。例如,您的for循环实际上不需要每次迭代都使用新的lambda,因为为了示例的缘故,假设所描述的结果是您想要编写的结果
var i =0;
Action<int> action = () => Debug.Print("Error: " + i);
for(;i<50;+i){
Task.Factory.StartNew(action);
}
第一个关闭i
,并在执行操作时使用i
的状态,该状态通常是循环完成后的状态。在后一种情况下,i
被急切地求值,因为它是作为参数传递给函数的。然后,此函数返回一个操作
,该操作被传递到StartNew
因此,设计决策使得惰性评估和急切评估都成为可能。懒惰是因为局部变量被关闭,而急切是因为您可以通过将局部变量作为参数传递或如下所示声明另一个作用域较短的局部变量来强制执行局部变量
for (var i = 0; i < 50; ++i) {
var j = i;
Task.Factory.StartNew(() => Debug.Print("Error: " + j));
}
他们为什么要这样做?无论如何,它们都是异步的。可能重复的“C#Captured Variable in Loop”IMHO,你在这里问了两个问题-标题中似乎有“真实”的问题(如何捕获值,以便任务在循环时根据值运行),但问题的主体似乎集中在“为什么这些事情会导致意外值?”(闭包捕获的效果意味着它们都引用了相同的变量)。因此,您最终得到的大多数答案都是解释行为的,而不是回答您的“真实”问题(AFAICT:)真的,詹姆斯,事实上,在阅读了最初的评论之后,我更改了我的问题标题,以更好地反映我的观点。你是说在lambda表达式中创建一个副本会起作用吗?目前不行:使用var a2=a;Logging.Print(“使用值:+a2”);仍然重新运行“使用值:after”。抱歉。您需要将副本放在lambda之外才能使其正常工作。情况很清楚,这里描述了:对于第一个循环,“正确”修复(AFAICT)是在循环内但在Task.Factory.StartNew之前执行var i1=i;的操作。通过该更改,每个闭包将引用其自己的单独变量,您将获得正确的效果。但是,状态参数避免了闭包的需要,因此肯定更有效,但如果您只想获得正确的行为,则不必这样做。不是这样的不起作用(这是语言的工作方式),它是lambda只能在循环之后开始执行finishes@James曼宁,你是对的,这只会在循环中创建一个局部变量,这样就不可能捕捉到错误的数据variable@PanagiotisKanavos-根据欧文的评论,如果
var i =0;
Action<int> action = () => Debug.Print("Error: " + i);
for(;i<50;+i){
Task.Factory.StartNew(action);
}
var i =0;
Func<Action<int>> action = (x) => { return () => Debug.Print("Error: " + x);}
for(;i<50;+i){
Task.Factory.StartNew(action(i));
}
for (var i = 0; i < 50; ++i) {
var j = i;
Task.Factory.StartNew(() => Debug.Print("Error: " + j));
}
var i =0;
Action<object> action = (x) => Debug.Print("Error: " + x);}
for(;i<50;+i){
Task.Factory.StartNew(action,i);
}