为什么在lambda表达式中使用迭代变量很糟糕

时间:2008-10-22 22:47:52

标签: vb.net lambda iteration warnings

我只是编写了一些快速代码并注意到了这个编译错误

  

在lambda表达式中使用迭代变量可能会产生意外结果   相反,在循环中创建一个局部变量并为其分配迭代变量的值。

我知道这意味着什么,我可以轻松解决它,而不是什么大不了的事 但我想知道为什么在lambda中使用迭代变量是个坏主意? 我以后可能会遇到什么问题?

3 个答案:

答案 0 :(得分:50)

考虑以下代码:

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年开始的,但实际行为可能已经发生了变化。

答案 1 :(得分:7)

假设你在这里意味着C#。

这是因为编译器实现闭包的方式。使用迭代变量可以导致访问修改后的闭包的问题(请注意,我说'可能'不会'会导致问题,因为有时它不会发生,具体取决于方法中的其他内容,有时你实际上想要访问修改后的闭包。)

更多信息:

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

答案 2 :(得分:0)

.NET中的闭包理论

Local variables: scope vs. lifetime (plus closures)(已归档2010)

(强调我的)

  

在这种情况下,我们使用了闭包。闭包只是驻留在方法外部的特殊结构,其中包含需要其他方法引用的局部变量。 当查询引用局部变量(或参数)时,该变量将由闭包捕获,并且对该变量的所有引用都将重定向到闭包。

在考虑闭包在.NET中的工作方式时,我建议牢记以下要点,这是设计人员在实现此功能时必须进行的工作:

  • 请注意,“变量捕获”和lambda表达式不是IL功能,VB.NET(和C#)必须使用现有工具(在这种情况下为类和Delegate)来实现这些功能。
  • 或者换句话说,局部变量实际上不能持久地超出其范围。该语言的作用是尽其所能使它看起来像 ,但这不是完美的抽象。
  • 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已被{{ 1}}的范围,而不是Sub VBDotNetSample()循环范围。

反编译的示例代码

我个人真的很想看看编译器为此生成了什么样的代码,所以我继续使用JetBrains DotPeek。我接受了编译器生成的代码,并将其手工翻译回VB.NET。

注释和变量名属于我。对该代码进行了略微简化,但不影响其行为。

For

请注意,对于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 的整个主体来说,closureHelperClass的实例只有一个,因此该函数无法打印中间的Sub CompilerGenerated循环索引值0, 1,2,3(没有存储这些值的位置)。该代码仅输出4(最终索引值)(在For循环之后)四次。

脚注:

  • 这篇文章中隐含了一个“ .NET 4.6.1版本”,但是在我看来,这些限制不太可能发生重大变化。如果找到无法重现这些结果的设置,请给我评论。

“但是jrh为什么您发布了较晚的答案?”

  • 这篇文章中链接的页面丢失或混乱。
  • 此vb.net标记问题上没有vb.net答案,截至撰写本文时,有C#(错误语言)答案和仅包含链接的答案(带有3条无效链接)。