为啥在 lambda 表达式中使用迭代变量不好
Posted
技术标签:
【中文标题】为啥在 lambda 表达式中使用迭代变量不好【英文标题】:Why is it bad to use an iteration variable in a lambda expression为什么在 lambda 表达式中使用迭代变量不好 【发布时间】:2010-09-18 16:11:30 【问题描述】:我只是在写一些快速代码,并注意到这个编译器错误
在 lambda 表达式中使用迭代变量可能会产生意想不到的结果。 相反,在循环中创建一个局部变量,并为其分配迭代变量的值。
我知道这意味着什么,我可以轻松修复它,这没什么大不了的。 但我想知道为什么在 lambda 中使用迭代变量是个坏主意? 我以后会引起什么问题?
【问题讨论】:
相关:***.com/questions/190227/… 如果你给出一个实际工作的例子/给出正确的结果会更好!例如,在这里查看结果pastebin.com/raw/FghmXkby 这是不对的.. 始终是相同的错误结果。 一个非常直观的实现,有 500,000 个问题和 9,000 篇关于它的博客文章......这是什么,C++? 【参考方案1】:假设您在这里指的是 C#。
这是因为编译器实现闭包的方式。使用迭代变量 can 会导致访问修改后的闭包出现问题(请注意,我说“不能”不是“将”导致问题,因为有时它不会发生,具体取决于方法中的其他内容,有时您实际上想访问修改后的闭包)。
更多信息:
http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx
更多信息:
http://blogs.msdn.com/oldnewthing/archive/2006/08/02/686456.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/03/687529.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx
【讨论】:
这不是“每个方法一个闭包”——它比这更复杂。 是的,我意识到读起来很糟糕——我试图快速解释这种情况(雷蒙德解释得更深入)。删除了冒犯性的短语,以便人们可以查看更多信息链接。 看起来链接已经失效,但您仍然可以在这里找到它们:devblogs.microsoft.com/oldnewthing/2006/08/page/4,“匿名方法的实现及其后果”(Raymond Chen / Old New Thing Blog)部分1 , 2, 3【参考方案2】:考虑这段代码:
List<Action> actions = new List<Action>();
for (int i = 0; i < 10; i++)
actions.Add(() => Console.WriteLine(i));
foreach (Action action in actions)
action();
您希望这会打印什么?显而易见的答案是 0...9 - 但实际上它打印了 10、10 次。这是因为只有一个变量被所有代表捕获。正是这种出乎意料的行为。
编辑:我刚刚看到您在谈论 VB.NET 而不是 C#。我相信 VB.NET 有更复杂的规则,因为变量在迭代中保持其值的方式。 This post by Jared Parsons 提供了一些有关所涉及的困难类型的信息 - 尽管它是从 2007 年开始的,因此从那时起实际行为可能已经改变。
【讨论】:
简而言之:循环时不需要对 lambda 进行评估,当调用它们时,迭代变量可能超出范围、未分配或具有最终值(甚至超出循环限制)。 @BertuPG:你想到的两个词是哪个? ;) @Joh:哦...是的...所以让我用“短语”替换“单词”^^ 我闻到了一个面试问题。 :-) 我注意到 VB 会显示问题中提到的警告,但 C# 不会(使用 VS2015 和 .NET 4.5.2)尽管行为相同(10 次、10 次)。不确定是否一直如此?【参考方案3】:.NET 中的闭包理论
Local variables: scope vs. lifetime (plus closures)(2010 年存档)
(强调我的)
在这种情况下,我们使用了闭包。闭包只是一种位于方法之外的特殊结构,其中包含需要由其他方法引用的局部变量。 当查询引用一个局部变量(或参数)时,该变量被闭包捕获,并且对该变量的所有引用都被重定向到闭包。
当您考虑闭包如何在 .NET 中工作时,我建议您牢记这些要点,这是设计人员在实现此功能时必须使用的内容:
请注意,“变量捕获”和 lambda 表达式不是 IL 功能,VB.NET(和 C#)必须使用现有工具(在本例中为类和Delegate
s)来实现这些功能。
或者换一种说法,局部变量实际上不能在其范围之外持久化。该语言所做的只是使其看起来像他们可以做到的那样,但这并不是一个完美的抽象。
Func(Of T)
(即Delegate
)实例无法存储传递给它们的参数。
虽然,Func(Of T)
确实存储了该方法所属的类的实例。这是 .NET 框架用来“记住”传递给 lambda 表达式的参数的途径。
让我们来看看吧!
示例代码:
假设你写了一些这样的代码:
' Prints 4,4,4,4
Sub VBDotNetSample()
Dim funcList As New List(Of Func(Of Integer))
For indexParameter As Integer = 0 To 3
'The compiler says:
' Warning BC42324 Using the iteration variable in a lambda expression may have unexpected results.
' Instead, create a local variable within the loop and assign it the value of the iteration variable
funcList.Add(Function()indexParameter)
Next
For Each lambdaFunc As Func(Of Integer) In funcList
Console.Write($"lambdaFunc()")
Next
End Sub
您可能希望代码打印 0,1,2,3,但实际上打印的是 4,4,4,4,这是因为 indexParameter
已在 Sub VBDotNetSample()
的范围内“捕获” s 范围,而不是在For
循环范围内。
反编译示例代码
就个人而言,我真的很想看看编译器为此生成了什么样的代码,所以我继续使用 JetBrains DotPeek。我将编译器生成的代码手动翻译回 VB.NET。
我的评论和变量名。代码在不影响代码行为的情况下略有简化。
Module Decompiledcode
' Prints 4,4,4,4
Sub CompilerGenerated()
Dim funcList As New List(Of Func(Of Integer))
'***********************************************************************************************
' There's only one instance of the closureHelperClass for the entire Sub
' That means that all the iterations of the for loop below are referencing
' the same class instance; that means that it can't remember the value of Local_indexParameter
' at each iteration, and it only remembers the last one (4).
'***********************************************************************************************
Dim closureHelperClass As New ClosureHelperClass_CompilerGenerated
For closureHelperClass.Local_indexParameter = 0 To 3
' NOTE that it refers to the Lambda *instance* method of the ClosureHelperClass_CompilerGenerated class,
' Remember that delegates implicitly carry the instance of the class in their Target
' property, it's not just referring to the Lambda method, it's referring to the Lambda
' method on the closureHelperClass instance of the class!
Dim closureHelperClassMethodFunc As Func(Of Integer) = AddressOf closureHelperClass.Lambda
funcList.Add(closureHelperClassMethodFunc)
Next
'closureHelperClass.Local_indexParameter is 4 now.
'Run each stored lambda expression (on the Delegate's Target, closureHelperClass)
For Each lambdaFunc As Func(Of Integer) in funcList
'The return value will always be 4, because it's just returning closureHelperClass.Local_indexParameter.
Dim retVal_AlwaysFour As Integer = lambdaFunc()
Console.Write($"retVal_AlwaysFour")
Next
End Sub
Friend NotInheritable Class ClosureHelperClass_CompilerGenerated
' Yes the compiler really does generate a class with public fields.
Public Local_indexParameter As Integer
'The body of your lambda expression goes here, note that this method
'takes no parameters and uses a field of this class (the stored parameter value) instead.
Friend Function Lambda() As Integer
Return Me.Local_indexParameter
End Function
End Class
End Module
注意Sub CompilerGenerated
的整个主体中只有一个closureHelperClass
实例,因此该函数无法打印中间的For
循环索引值0、1、2、3(没有地方存储这些值)。该代码只打印了 4,即最终索引值(在 For
循环之后)四次。
脚注:
在这篇文章中隐含了“截至 .NET 4.6.1”,但在我看来,这些限制不太可能发生巨大变化;如果您发现无法重现这些结果的设置,请给我留言。“但是 jrh,你为什么发布迟到的答案?”
这篇文章中链接的页面要么丢失,要么乱七八糟。 在这个带有 vb.net 标记的问题上没有 vb.net 答案,截至撰写本文时,有一个 C#(错误语言)答案和一个仅链接的答案(带有 3 个死链接)。【讨论】:
仅供参考,如果其他人在玩代码,并且当您重命名closureHelperClass
时您会遇到桌面崩溃,看起来就像是 due to a bug in Visual Studio,使用重命名/重构时经常保存!
以上是关于为啥在 lambda 表达式中使用迭代变量不好的主要内容,如果未能解决你的问题,请参考以下文章
为啥在未计算的操作数中不允许使用 lambda 表达式,但在常量表达式的未计算部分中允许使用 lambda 表达式?