Pregunta

Estaba escribiendo un código rápido y noté este error de compatibilidad

  

El uso de la variable de iteración en una expresión lambda puede tener resultados inesperados.
  En su lugar, cree una variable local dentro del bucle y asígnele el valor de la variable de iteración.

Sé lo que significa y puedo arreglarlo fácilmente, no es un gran problema.
Pero me preguntaba por qué es una mala idea usar una variable de iteración en un lambda?
¿Qué problemas puedo causar más adelante?

¿Fue útil?

Solución

Considera 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();
}

¿Qué esperas que se imprima esto? La respuesta obvia es 0 ... 9, pero en realidad se imprime 10, diez veces. Es porque solo hay una variable que es capturada por todos los delegados. Es este tipo de comportamiento lo que es inesperado.

EDITAR: Acabo de ver que estás hablando de VB.NET en lugar de C #. Creo que VB.NET tiene reglas aún más complicadas, debido a la forma en que las variables mantienen sus valores en las iteraciones. Esta publicación por Jared Parsons brinda información sobre el tipo de dificultades involucradas, aunque ya regresó de 2007, por lo que el comportamiento real puede haber cambiado desde entonces.

Otros consejos

Suponiendo que te refieres a C # aquí.

Es debido a la forma en que el compilador implementa los cierres. El uso de una variable de iteración puede causar un problema al acceder a un cierre modificado (tenga en cuenta que dije que 'no' puede 'causará' un problema porque a veces no sucede dependiendo de lo que haya en el método y, a veces, realmente desea acceder al cierre modificado).

Más información:

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

Aún más información:

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

Teoría de cierres en .NET

Variables locales: alcance vs. duración (más cierres) (Archivado 2010)

(énfasis mío)

  

Lo que sucede en este caso es que usamos un cierre. Un cierre es solo una estructura especial que vive fuera del método que contiene las variables locales a las que se debe hacer referencia mediante otros métodos. Cuando una consulta se refiere a una variable (o parámetro) local, esa variable es capturada por el cierre y todas las referencias a la variable se redirigen al cierre.

Cuando esté pensando en cómo funcionan los cierres en .NET, le recomiendo que tenga en cuenta estos puntos, esto es con lo que tuvieron que trabajar los diseñadores cuando implementaron esta función:

  • Tenga en cuenta que " captura de variables " y las expresiones lambda no son una función de IL, VB.NET (y C #) tuvieron que implementar estas funciones utilizando las herramientas existentes, en este caso, las clases y las Delegate s.
  • O para decirlo de otra manera, las variables locales no pueden persistir más allá de su alcance. Lo que el lenguaje hace es hacer que parezca como pueden, pero no es una abstracción perfecta.
  • Las instancias de
  • Func (Of T) (es decir, Delegate ) no tienen forma de almacenar los parámetros que se les pasan.
  • Aunque, Func (Of T) sí almacena la instancia de la clase de la que forma parte el método. Esta es la avenida que usaba el framework .NET para " recordar " parámetros pasados ??a expresiones lambda.

Bueno, echemos un vistazo!

Código de ejemplo:

Digamos que escribiste un 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(
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(<*>quot;{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
quot;{lambdaFunc()}") Next End Sub

Puede estar esperando que el código se imprima 0,1,2,3, pero en realidad se imprime 4,4,4,4, esto se debe a que indexParameter se ha capturado " " en el alcance del alcance de Sub VBDotNetSample () , y no en el alcance del bucle de For .

Código de ejemplo descompilado

Personalmente, realmente quería ver qué tipo de código generó el compilador para esto, así que seguí adelante y usé JetBrains DotPeek. Tomé el código generado por el compilador y lo traduje a mano a VB.NET.

Comentarios y nombres de variables míos. El código se simplificó ligeramente de manera que no afecte el comportamiento del código.

<*>

Observe cómo solo hay una instancia de closingHelperClass para todo el cuerpo de Sub CompilerGenerated , por lo que no hay forma de que la función pueda imprimir el intermedio para valores de índice de bucle de 0,1,2,3 (no hay lugar para almacenar estos valores). El código solo imprime 4, el valor del índice final (después del bucle For ) cuatro veces.

Notas al pie:

  • Hay un implícito " A partir de .NET 4.6.1 " en este post, pero en mi opinión es muy poco probable que estas limitaciones cambien dramáticamente; Si encuentra una configuración en la que no puede reproducir estos resultados, déjeme un comentario.

" Pero jrh, ¿por qué publicaste una respuesta tardía? "

  • Las páginas enlazadas en esta publicación faltan o están en ruinas.
  • No hubo una respuesta de vb.net en esta pregunta etiquetada de vb.net, en el momento de escribir esto hay una respuesta de C # (lenguaje incorrecto) y una respuesta mayoritariamente solo de enlaces (con 3 enlaces muertos).
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top