Domanda

Stavo solo scrivendo un po 'di codice rapido e ho notato questo errore complier

  

L'uso della variabile di iterazione in un'espressione lambda può avere risultati imprevisti.
  Invece, crea una variabile locale all'interno del ciclo e assegnagli il valore della variabile di iterazione.

So cosa significa e posso facilmente risolverlo, non è un grosso problema.
Ma mi chiedevo perché è una cattiva idea usare una variabile di iterazione in una lambda?
Quali problemi posso causare in seguito?

È stato utile?

Soluzione

Considera questo codice:

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

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

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

Cosa ti aspetti che venga stampato? La risposta ovvia è 0 ... 9 - ma in realtà stampa 10, dieci volte. È perché c'è solo una variabile che viene catturata da tutti i delegati. È questo tipo di comportamento che è inaspettato.

EDIT: ho appena visto che stai parlando di VB.NET piuttosto che di C #. Credo che VB.NET abbia regole ancora più complicate, a causa del modo in cui le variabili mantengono i loro valori attraverso le iterazioni. Questo post di Jared Parsons fornisce alcune informazioni sul tipo di difficoltà coinvolte, anche se è tornato dal 2007, quindi il comportamento reale potrebbe essere cambiato da allora.

Altri suggerimenti

Supponendo che intendi C # qui.

È a causa del modo in cui il compilatore implementa le chiusure. L'uso di una variabile di iterazione può causare un problema con l'accesso a una chiusura modificata (nota che ho detto che 'non puoi' causerà 'un problema perché a volte non accade a seconda di cos'altro c'è nel metodo e talvolta desideri effettivamente accedere alla chiusura modificata).

Ulteriori informazioni:

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

Altre informazioni:

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 delle chiusure in .NET

Variabili locali: ambito vs. durata (più chiusure) (Archiviato 2010)

(Enfasi mia)

  

Quello che succede in questo caso è che usiamo una chiusura. Una chiusura è solo una struttura speciale che vive al di fuori del metodo che contiene le variabili locali che devono essere citate da altri metodi. Quando una query fa riferimento a una variabile (o parametro) locale, tale variabile viene acquisita dalla chiusura e tutti i riferimenti alla variabile vengono reindirizzati alla chiusura.

Quando stai pensando a come funzionano le chiusure in .NET, ti consiglio di tenere a mente questi punti elenco, questo è ciò con cui i progettisti hanno dovuto lavorare durante l'implementazione di questa funzione:

  • Nota che "cattura variabile" e le espressioni lambda non sono una funzionalità IL, VB.NET (e C #) hanno dovuto implementare queste funzionalità utilizzando strumenti esistenti, in questo caso classi e Delegate .
  • O per dirla in altro modo, le variabili locali non possono davvero essere mantenute oltre il loro scopo. Ciò che la lingua fa è far sembrare come possono, ma non è un'astrazione perfetta.
  • Le istanze
  • Func (Of T) (ovvero, Delegate ) non hanno modo di memorizzare i parametri passati in esse.
  • Tuttavia, Func (Of T) memorizza l'istanza della classe di cui fa parte il metodo. Questa è la strada utilizzata dal framework .NET per "ricordare" parametri passati nelle espressioni lambda.

Bene diamo un'occhiata!

Codice di esempio:

Quindi diciamo che hai scritto un codice come questo:

' 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

Potresti aspettarti che il codice stampi 0,1,2,3, ma in realtà stampa 4,4,4,4, questo perché indexParameter è stato "catturato". nell'ambito di ambito VBDotNetSample () e non nell'ambito di ciclo For .

Codice di esempio decompilato

Personalmente, volevo davvero vedere che tipo di codice ha generato il compilatore per questo, quindi sono andato avanti e ho usato JetBrains DotPeek. Ho preso il codice generato dal compilatore e tradotto a mano in VB.NET.

Commenti e nomi di variabili miei. Il codice è stato leggermente semplificato in modi che non influiscono sul comportamento del codice.

<*>

Nota come esiste una sola istanza di closingHelperClass per l'intero corpo di Sub CompilerGenerated , quindi non è possibile che la funzione stampi il intermedio per valori di indice di loop di 0,1,2,3 (non c'è spazio per memorizzare questi valori). Il codice stampa solo 4, il valore dell'indice finale (dopo il ciclo For ) quattro volte.

Note:

  • È implicito un "A partire da .NET 4.6.1" in questo post, ma secondo me è molto improbabile che queste limitazioni cambino radicalmente; se trovi una configurazione in cui non puoi riprodurre questi risultati, per favore lasciami un commento.

" Ma jrh perché hai postato una risposta in ritardo? "

  • Le pagine collegate in questo post sono mancanti o in frantumi.
  • Non c'è stata risposta vb.net su questa domanda taggata su vb.net, al momento della stesura di questo articolo c'è una risposta C # (lingua sbagliata) e una risposta principalmente solo link (con 3 link morti).
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top