.NET 中的闭包理论
Local variables: scope vs. lifetime (plus closures)(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,4,这是因为 indexParameter 已在 Sub VBDotNetSample() 的范围内“捕获” s 范围,而不是在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 循环索引值0、1、2、3(没有地方存储这些值)。该代码只打印了 4,即最终索引值(在 For 循环之后)四次。
脚注:
- 在这篇文章中隐含了“截至 .NET 4.6.1”,但在我看来,这些限制不太可能发生巨大变化;如果您发现无法重现这些结果的设置,请给我留言。
“但是 jrh,你为什么发布迟到的答案?”
- 这篇文章中链接的页面要么丢失,要么乱七八糟。
- 在这个带有 vb.net 标记的问题上没有 vb.net 答案,截至撰写本文时,有一个 C#(错误语言)答案和一个仅链接的答案(带有 3 个死链接)。