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

本文关键字:变量 迭代 lambda 表达式 为什么 | 更新日期: 2023-09-27 17:48:51

我刚刚写了一些快速代码,注意到这个编译器错误

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

我知道这意味着什么,我可以很容易地解决它,没什么大不了的
但我想知道为什么在lambda中使用迭代变量是个坏主意
以后我会引起什么问题?

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

考虑以下代码:

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有更复杂的规则,这是因为变量在迭代中保持其值的方式。贾里德·帕森斯的这篇帖子提供了一些关于所涉及的困难的信息——尽管它是从2007年开始的,所以从那时起,实际行为可能发生了变化。

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

.NET中的闭包理论

局部变量:范围与寿命(加上闭包)(存档于2010年)

(强调矿)

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

当你考虑闭包在.NET中是如何工作的时,我建议你记住这些要点,这是设计师在实现这个功能时必须使用的:

  • 注意,";变量捕获";和lambda表达式不是IL功能,VB.NET(和C#)必须使用现有的工具来实现这些功能,在本例中是类和Delegates
  • 或者换一种说法,局部变量不能真正持久化到超出它们的范围。该语言所做的是让看起来像,但它不是一个完美的抽象
  • 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,这是因为indexParameter一直是"0";捕获的";在Sub VBDotNetSample()的作用域中,而不在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实例,因此函数无法打印0、1、2、3的中间For循环索引值(没有存储这些值的地方)。该代码只打印4次,即最终索引值(在For循环之后)。

脚注:

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

"但是jrh,你为什么迟交答案"

  • 这篇文章中链接的页面要么不见了,要么一团糟
  • 这个带有vb.net标签的问题没有vb.net答案,截至撰写本文时,有一个C#(错误语言)答案和一个主要只链接的答案(有3个死链接)