Почему плохо использовать переменную итерации в лямбда-выражении

StackOverflow https://stackoverflow.com/questions/227820

Вопрос

Я просто писал небольшой код и заметил эту ошибку компилятора.

Использование переменной итерации в лямбда-выражении может привести к неожиданным результатам.
Вместо этого создайте локальную переменную внутри цикла и присвойте ей значение переменной итерации.

Я знаю, что это значит, и легко могу это исправить, в этом нет ничего страшного.
Но мне было интересно, почему использовать переменную итерации в лямбде — плохая идея?
Какие проблемы я могу вызвать в дальнейшем?

Это было полезно?

Решение

Рассмотрим этот код:

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 мертвыми ссылками).
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top