Теория замыканий в .NET
Локальные переменные: объем и время жизни (плюс замыкания) (в архиве 2010)
(Акцент мой)
Что происходит в этом случае, мы используем закрытие. Замыкание - это просто специальная структура, которая находится вне метода и содержит локальные переменные, на которые должны ссылаться другие методы. Когда запрос ссылается на локальную переменную (или параметр), эта переменная захватывается замыканием, и все ссылки на переменную перенаправляются в замыкание.
Когда вы думаете о том, как замыкания работают в .NET, я рекомендую помнить об этих пунктах, с которыми разработчикам приходилось работать при реализации этой функции:
- Обратите внимание, что "захват переменных" и лямбда-выражения не являются функцией IL, VB.NET (и C #) пришлось реализовать эти функции с использованием существующих инструментов, в данном случае классов и
Delegate
s.
- Или, другими словами, локальные переменные не могут сохраняться за пределами их области видимости. Что делает язык, так это заставляет его казаться таким, каким они могут, но это не идеальная абстракция.
Func(Of T)
(т.е. Delegate
) экземпляры не имеют возможности хранить переданные в них параметры.
- Хотя,
Func(Of T)
хранит экземпляр класса, частью которого является метод. Это путь .NET Framework, используемый для «запоминания» параметров, переданных в лямбда-выражения.
Хорошо, давайте посмотрим!
Пример кода:
Допустим, вы написали такой код:
' 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 недействительными ссылками).