Pergunta

Eu estava escrevendo um código rápido e percebi esse erro de conformidade

Usar a variável de iteração em uma expressão lambda pode ter resultados inesperados.
Em vez disso, crie uma variável local dentro do loop e atribua a ela o valor da variável de iteração.

Eu sei o que isso significa e posso consertar facilmente, o que não é grande coisa.
Mas eu queria saber por que é uma má ideia usar uma variável de iteração em um lambda?
Que problemas posso causar mais tarde?

Foi útil?

Solução

Considere este código:

List<Action> actions = new List<Action>();

for (int i = 0; i < 10; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (Action action in actions)
{
    action();
}

O que você esperaria que isso fosse impresso?A resposta óbvia é 0...9 - mas na verdade ela imprime 10, dez vezes.É porque há apenas uma variável que é capturada por todos os delegados.É esse tipo de comportamento que é inesperado.

EDITAR:Acabei de ver que você está falando sobre VB.NET e não sobre C#.Acredito que o VB.NET tenha regras ainda mais complicadas, devido à maneira como as variáveis ​​mantêm seus valores ao longo das iterações. Esta postagem de Jared Parsons fornece algumas informações sobre o tipo de dificuldades envolvidas - embora seja de 2007, o comportamento real pode ter mudado desde então.

Outras dicas

Supondo que você quer dizer C# aqui.

É por causa da maneira como o compilador implementa o fechamento. Usando uma variável de iteração posso causar um problema em acessar um fechamento modificado (observe que eu disse que 'pode' não 'causará um problema, porque às vezes isso não acontece dependendo do que mais está no método, e às vezes você realmente deseja acessar o fechamento modificado) .

Mais informações:

http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx

Ainda mais informações:

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

Teoria dos fechamentos em .NET

Variáveis ​​locais:escopo vs.vitalício (mais fechamentos) (Arquivado em 2010)

(ênfase minha)

O que acontece neste caso é que usamos um encerramento.Um encerramento é apenas uma estrutura especial que vive fora do método que contém as variáveis ​​locais que precisam ser referenciadas por outros métodos. Quando uma consulta se refere a uma variável local (ou parâmetro), essa variável é capturada pelo encerramento e todas as referências à variável são redirecionadas para o encerramento.

Quando você está pensando em como os encerramentos funcionam no .NET, recomendo manter estes pontos em mente, é com isso que os designers tiveram que trabalhar quando estavam implementando esse recurso:

  • Observe que "captura de variável" e expressões lambda não são um recurso IL, o VB.NET (e C#) teve que implementar esses recursos usando ferramentas existentes, neste caso, classes e DelegateS.
  • Ou, dito de outra forma, as variáveis ​​locais não podem realmente ser persistidas além do seu escopo.O que a linguagem faz é torná-la parecer como podem, mas não é uma abstração perfeita.
  • Func(Of T) (ou seja, Delegate) não têm como armazenar parâmetros passados ​​para elas.
  • No entanto, Func(Of T) armazene a instância da classe da qual o método faz parte.Este é o caminho que o .NET framework usa para "lembrar" parâmetros passados ​​em expressões lambda.

Bem, vamos dar uma olhada!

Código de amostra:

Então, digamos que você escreveu algum código como este:

' 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

Você pode estar esperando que o código imprima 0,1,2,3, mas na verdade ele imprime 4,4,4,4, isso ocorre porque indexParameter foi "capturado" no âmbito da Sub VBDotNetSample()escopo, e não no For escopo do loop.

Código de exemplo descompilado

Pessoalmente, eu realmente queria ver que tipo de código o compilador gerava para isso, então fui em frente e usei o JetBrains DotPeek.Peguei o código gerado pelo compilador e o traduzi manualmente de volta para VB.NET.

Comentários e nomes de variáveis ​​são meus.O código foi ligeiramente simplificado de uma forma que não afeta o comportamento do código.

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

Observe como há apenas uma instância de closureHelperClass para todo o corpo de Sub CompilerGenerated, então não há como a função imprimir o intermediário For valores de índice de loop de 0,1,2,3 (não há lugar para armazenar esses valores).O código imprime apenas 4, o valor final do índice (após o For loop) quatro vezes.

Notas de rodapé:

  • Há um "A partir do .NET 4.6.1" implícito nesta postagem, mas na minha opinião é muito improvável que essas limitações mudem drasticamente;se você encontrar uma configuração em que não consiga reproduzir esses resultados, deixe-me um comentário.

"Mas jrh, por que você postou uma resposta tardia?"

  • As páginas vinculadas nesta postagem estão desaparecidas ou em ruínas.
  • Não houve resposta vb.net para esta pergunta marcada com vb.net, no momento em que este artigo foi escrito, havia uma resposta em C # (linguagem errada) e uma resposta principalmente apenas de link (com 3 links inativos).
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top