C# lambda,局部变量值未取时你认为?
Posted
技术标签:
【中文标题】C# lambda,局部变量值未取时你认为?【英文标题】:C# lambda, local variable value not taken when you think? 【发布时间】:2011-05-08 12:40:32 【问题描述】:假设我们有以下代码:
void AFunction()
foreach(AClass i in AClassCollection)
listOfLambdaFunctions.AddLast( () => PrintLine(i.name); );
void Main()
AFunction();
foreach( var i in listOfLambdaFunctions)
i();
有人可能会认为上面的代码与下面的代码相同:
void Main()
foreach(AClass i in AClassCollection)
PrintLine(i.name);
然而,事实并非如此。相反,它每次都打印 AClassCollection 中最后一项的名称。
看起来好像每个 lambda 函数都使用了相同的项目。我怀疑从 创建 lambda 时到 当 lambda 为其中使用的外部变量拍摄快照时可能会有一些延迟。
本质上,lambda 持有对局部变量 i
的引用,而不是在创建 lambda 时获取 i
值的“快照”。
为了测试这个理论,我尝试了这段代码:
string astr = "a string";
AFunc fnc = () => System.Diagnostics.Debug.WriteLine(astr); ;
astr = "changed";
fnc();
而且,惊喜,它输出changed
!
我使用的是 XNA 3.1,以及它附带的任何 C# 版本。
我的问题是:
-
这是怎么回事?
lambda 函数是否以某种方式存储对变量的“引用”或其他内容?
有没有办法解决这个问题?
【问题讨论】:
相关:***.com/questions/271440/c-captured-variable-in-loop 怎么能输出“改变了!”当你分配“改变!”? :p “闭包关闭变量,而不是值”规则在这里应用,所以不要期望 asr 的值被复制到那个 lambda 函数中,你复制了变量所以,在这种情况下它是引用该变量,并且您已经通过自己的观察证明了这一点。 【参考方案1】:lambda 函数是否以某种方式存储对变量的“引用”或其他内容?
关闭。 lambda 函数捕获变量 本身。没有必要将 reference 存储到变量中,事实上,在 .NET 中,永久存储对变量的引用是不可能的。您只需捕获整个变量。你永远不会捕获变量的值。
请记住,变量是一个存储位置。名称“i”指的是特定的存储位置,在您的情况下,它总是指的是相同的存储位置。
有没有办法解决这个问题?
是的。每次通过循环创建一个新变量。然后闭包每次都会捕获一个不同的变量。
这是最常报告的 C# 问题之一。我们正在考虑更改循环变量声明的语义,以便每次循环都创建一个新变量。
有关此问题的更多详细信息,请参阅我有关该主题的文章:
http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/
【讨论】:
就您有时 Eric 所说的成本/收益计算而言,对此进行更改会比错误修复或新功能更有价值吗? @asawyer:这是个好问题。这可能是我们得到的第一个“错误”错误报告。它让很多人感到困惑。关闭循环变量的代码很脆弱,几乎没有人真正想要指定的行为。无论如何,我们将对关闭代码进行大手术,以使“等待”工作;我们可以解决这个问题。这些都是采取行动的充分理由。另一方面,这是一个突破性的变化,它分散了对更重要工作的注意力。我敢肯定,设计团队会在某个时候以一种或另一种方式提出建议。【参考方案2】:我也被这个抓住了,正如 Calgary Coder 所说,它是一个修改过的闭包。在我重新锐化之前,我真的很难发现它们。由于它是 resharper 关注的警告之一,因此我更擅长在编码时识别它们。
【讨论】:
【参考方案3】:是的,lambda 存储对变量的引用(从概念上讲,无论如何)。
一个非常简单的解决方法是:
foreach(AClass i in AClassCollection)
AClass j = i;
listOfLambdaFunctions.AddLast( () => PrintLine(j.name); );
在 foreach 循环的每次迭代中,都会创建一个 new j
,lambda 会捕获它。
另一方面,i
始终是同一个变量,但每次迭代都会更新(因此所有 lambdas 最终都会看到最后一个值)
我同意这有点令人惊讶。 :)
【讨论】:
【参考方案4】:发生了什么事? lambda函数是否以某种方式存储对变量的“引用”或其他东西?
是的正是; c# 捕获的变量是 variable,而不是变量的 value。你通常可以通过引入一个临时变量并绑定到它来解决这个问题:
string astr = "a string";
var tmp = astr;
AFunc fnc = () => System.Diagnostics.Debug.WriteLine(tmp); ;
尤其是在 foreach
中臭名昭著的地方。
【讨论】:
【参考方案5】:这是一个修改过的闭包
参见:Access to Modified Closure等类似问题
要解决此问题,您必须在 for 循环范围内存储变量的副本:
foreach(AClass i in AClassCollection)
AClass anotherI= i;
listOfLambdaFunctions.AddLast( () => PrintLine(anotherI.name); );
【讨论】:
啊,是的!现在它向我指出了这样一个明显的解决方案:D。我总是忘记从技术上讲,每个循环都会创建并清除一个这样的变量(在 c++ 中),或者只是在每个循环中创建一个新的“变量”。主要是因为它让我内心的最佳代码纳粹一想到实际发生的事情就哭了:),不过在这里肯定很方便! @matt,考虑到这些变量是在每个循环中重新创建的,并不意味着它们必须在编译版本中实现没有必要。同时,它确实允许上述工作。两全其美。以上是关于C# lambda,局部变量值未取时你认为?的主要内容,如果未能解决你的问题,请参考以下文章