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

在 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有更复杂的规则,因为变量在迭代中保持其值的方式。 这篇文章作者:贾里德·帕森斯(Jared Parsons)提供了一些关于所涉及的困难的信息 - 虽然它是从2007年开始的,但实际行为可能已经发生了变化。

其他提示

.NET 中的闭包理论

局部变量:范围对比生命周期(加上闭包) (2010 年存档)

(强调我的)

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

当您考虑闭包在 .NET 中如何工作时,我建议您牢记这些要点,这是设计人员在实现此功能时必须处理的内容:

  • 请注意,“变量捕获”和 lambda 表达式不是 IL 功能,VB.NET(和 C#)必须使用现有工具(在本例中为类和 Delegates。
  • 或者换句话说,局部变量实际上不能在其范围之外持久保存。语言所做的就是创造它 似乎 就像他们可以的那样,但这并不是一个完美的抽象。
  • Func(Of T) (IE。, 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()的范围,而不是在 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

请注意,只有一个实例 closureHelperClass 对于整个身体 Sub CompilerGenerated, ,所以该函数无法打印中间结果 For 循环索引值 0,1,2,3(没有地方存储这些值)。该代码仅打印 4,即最终索引值(在 For 循环)四次。

脚注:

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

“但是你为什么这么晚才回复呢?”

  • 这篇文章中链接的页面要么丢失,要么乱七八糟。
  • 对于这个 vb.net 标记问题没有 vb.net 答案,截至撰写本文时,有一个 C#(错误语言)答案和一个主要仅链接的答案(有 3 个死链接)。
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top