Почему плохо использовать итерационную переменную в лямбда-выражении - PullRequest
51 голосов
/ 23 октября 2008

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

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

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

Ответы [ 3 ]

50 голосов
/ 23 октября 2008

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

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 года, поэтому с тех пор реальное поведение могло измениться.

7 голосов
/ 23 октября 2008

Предполагая, что вы имеете в виду 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

0 голосов
/ 23 марта 2019

Теория замыканий в .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 недействительными ссылками).
...