我刚刚在编写一些快速代码时发现了这个编译器错误
在 lambda 表达式中使用迭代变量可能会导致意外结果。
相反,在循环内创建一个局部变量并将其赋值为迭代变量的值。
我知道这是什么意思,我可以很容易地解决它,不是什么大问题。
但我想知道为什么在 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,十次。因为所有的委托都捕获同一个变量。这种行为是意料之外的。
编辑:我刚看到你谈论的是VB.NET而不是C#。我相信由于变量在迭代中维护其值的方式,VB.NET有更复杂的规则。 Jared Parsons在此篇文章中提供了一些信息,解释了所涉及的困难 - 尽管它是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
本地变量:范围 vs. 生命周期 (加上闭包) (存档于2010年)
(重点是我的)
在这种情况下发生的是我们使用闭包。闭包只是一个特殊的结构,存在于包含需要被其他方法引用的局部变量的方法之外。 当查询引用局部变量(或参数)时,该变量由闭包捕获,并将对变量的所有引用重定向到闭包。
当您考虑.NET中闭包的工作原理时,我建议记住以下要点,这是设计者在实现此功能时必须使用的内容:
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
已经被"捕获"在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
实例,因此函数无法打印中间的 For
循环索引值(没有地方存储这些值)。该代码仅打印 4,即最终索引值(在 For
循环之后)四次。"但是 jrh,为什么你要发表一篇晚回答的文章?"
closureHelperClass
导致程序崩溃并直接退出桌面的话,看起来这是由于Visual Studio中的一个错误引起的(https://github.com/dotnet/roslyn/issues/21662),在使用重命名/重构时请经常保存! - jrh