Почему плохо использовать переменную итерации в лямбда-выражении
Вопрос
Я просто писал небольшой код и заметил эту ошибку компилятора.
Использование переменной итерации в лямбда-выражении может привести к неожиданным результатам.
Вместо этого создайте локальную переменную внутри цикла и присвойте ей значение переменной итерации.
Я знаю, что это значит, и легко могу это исправить, в этом нет ничего страшного.
Но мне было интересно, почему использовать переменную итерации в лямбде — плохая идея?
Какие проблемы я могу вызвать в дальнейшем?
Решение
Рассмотрим этот код:
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 имеет еще более сложные правила, потому что переменные поддерживают свои значения на протяжении итераций. Этот пост Джаред Парсонс дает некоторую информацию о типе связанных с этим трудностей - хотя это еще с 2007 года, поэтому реальное поведение могло измениться с тех пор.
Другие советы
Предполагая, что вы имеете в виду C # здесь.
Это из-за способа, которым компилятор реализует замыкания. Использование итерационной переменной can вызывает проблему с доступом к измененному закрытию (обратите внимание, что я сказал, что 'can' not 'вызовет' проблему, потому что иногда это не происходит в зависимости от того, что еще находится в методе и иногда вы действительно хотите получить доступ к измененному закрытию).
Дополнительная информация:
http://blogs.msdn.com/abhinaba/ Архив / 2005/10/18 / 482180.aspx
Еще больше информации:
http://blogs.msdn.com/oldnewthing/ Архив / 2006/08/02 / 686456.aspx
http://blogs.msdn.com/oldnewthing/ Архив / 2006/08/03 / 687529.aspx
http://blogs.msdn.com/oldnewthing/ Архив / 2006/08/04 / 688527.aspx
Теория замыканий в .NET
Локальные переменные:объем против.срок службы (плюс закрытия) (Архивировано в 2010 г.)
(Выделено мной)
В этом случае мы используем замыкание.Замыкание — это просто специальная структура, находящаяся вне метода и содержащая локальные переменные, на которые должны ссылаться другие методы. Когда запрос ссылается на локальную переменную (или параметр), эта переменная захватывается замыканием, и все ссылки на переменную перенаправляются в замыкание.
Когда вы думаете о том, как замыкания работают в .NET, я рекомендую помнить об этих пунктах. Это то, с чем пришлось работать дизайнерам, когда они реализовывали эту функцию:
- Обратите внимание, что «захват переменных» и лямбда-выражения не являются функцией IL, VB.NET (и C#) пришлось реализовывать эти функции с использованием существующих инструментов, в данном случае классов и
Delegate
с. - Или, говоря иначе, локальные переменные не могут сохраняться за пределами их области действия.Язык делает это казаться вроде могут, но это не идеальная абстракция.
Func(Of T)
(т. е.,Delegate
) экземпляры не имеют возможности хранить переданные в них параметры.- Хотя,
Func(Of T)
сохраните экземпляр класса, частью которого является метод.Это способ, который платформа .NET использует для «запоминания» параметров, передаваемых в лямбда-выражения.
Ну давайте посмотрим!
Образец кода:
Допустим, вы написали такой код:
' 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 мертвыми ссылками).