为什么在 lambda 表达式中使用迭代变量不好

Why is it bad to use an iteration variable in a lambda expression

提问人:Nathan W 提问时间:10/23/2008 最后编辑:Theodor ZouliasNathan W 更新时间:9/2/2022 访问量:16062

问:

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

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

我知道这意味着什么,我可以轻松修复它,没什么大不了的。
但是我想知道为什么在 lambda 中使用迭代变量是个坏主意?
我以后会造成什么问题?

C# vb.net Lambda 迭代 警告

评论

0赞 nawfal 11/2/2013
相关新闻: stackoverflow.com/questions/190227/...
0赞 barlop 3/13/2017
如果你举一个例子,它实际工作/给出正确的结果,那就更好了!例如,看看这里的结果 pastebin.com/raw/FghmXkby 这是不对的。始终如一的错误结果。
0赞 jrh 3/22/2019
一个令人惊讶的直观实现,以至于有 500,000 个问题和 9,000 篇关于它的博客文章......这是什么,C++?

答:

8赞 Greg Beech 10/23/2008 #1

假设您在这里指的是 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

评论

1赞 Jon Skeet 10/23/2008
它不是“每种方法一个闭包”——它比这更复杂。
1赞 Greg Beech 10/23/2008
是的,我意识到读得很糟糕——我试图快速解释这种情况(雷蒙德更深入地解释)。删除了冒犯性的短语,以便人们可以查看更多信息链接。
1赞 jrh 3/22/2019
看起来链接已经死了,但你仍然可以在这里找到它们: devblogs.microsoft.com/oldnewthing/2006/08/page/4 , “匿名方法的实现及其后果” (Raymond Chen / Old New Thing Blog) part 123
56赞 Jon Skeet 10/23/2008 #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 的规则更加复杂,因为变量在迭代中保持其值的方式。贾里德·帕森斯(Jared Parsons)的这篇文章提供了一些关于所涉及的困难的信息 - 尽管它是从2007年开始的,所以从那时起实际行为可能已经发生了变化。

评论

6赞 BertuPG 2/8/2011
简而言之:lambda 在循环时不一定被计算,当它们被调用时,迭代变量可能超出范围、未分配或具有其最终值(甚至超出循环限制)。
10赞 Jon Skeet 2/8/2011
@BertuPG:你想到的两个词是哪一个?;)
0赞 BertuPG 2/9/2011
@Joh: 噢...是的。。。因此,让我用“短语”代替“单词”^^
4赞 EightyOne Unite 7/7/2011
我闻到了一个面试问题。:-)
2赞 41686d6564 6/23/2019
我注意到 VB 显示问题中提到的警告,但 C# 没有(使用 VS2015 和 .NET 4.5.2),尽管行为相同(10 次、10 次)。不确定是否一直如此?
4赞 4 revsjrh #3

.NET 中的闭包理论

局部变量:作用域与生存期(加上闭包)(存档于 2010 年)

(强调我的)

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

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

  • 请注意,“变量捕获”和 lambda 表达式不是 IL 功能,VB.NET(和 C#)必须使用现有工具(在本例中为 classes 和 s)实现这些功能。Delegate
  • 或者换句话说,局部变量不能真正超出其范围持久化。语言所做的是让它看起来可以,但它不是一个完美的抽象。
  • Func(Of T)(即 ) 实例无法存储传递给它们的参数。Delegate
  • 但是,请存储该方法所属类的实例。这是 .NET Framework 用于“记住”传递给 lambda 表达式的参数的途径。Func(Of T)

好吧,让我们来看看吧!

示例代码:

假设你写了一些这样的代码:

' 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,4,这是因为已在 的作用域中“捕获”,而不是在循环作用域中。indexParameterSub VBDotNetSample()For

反编译示例代码

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

注释和变量名称 mine。代码以不影响代码行为的方式略微简化。

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

请注意,的整个主体只有一个实例 ,因此该函数无法打印中间循环索引值 0,1,2,3(没有位置存储这些值)。该代码只打印 4 次,最终索引值(循环后)四次。closureHelperClassSub CompilerGeneratedForFor

脚注:

  • 这篇文章中隐含着“从 .NET 4.6.1 开始”,但在我看来,这些限制不太可能发生巨大变化;如果您发现无法重现这些结果的设置,请给我留言。

“但是,你为什么迟到回答?”

  • 这篇文章中链接的页面要么丢失,要么一团糟。
  • 关于这个 vb.net 标记的问题没有 vb.net 答案,截至撰写本文时,有一个 C#(错误语言)答案和一个大部分仅链接的答案(有 3 个死链接)。

评论

0赞 jrh 4/10/2019
仅供参考,如果其他人正在玩代码,并且在重命名时桌面上出现硬崩溃,看起来这是由于 Visual Studio 中的错误,在使用重命名/重构时经常保存!closureHelperClass