Question

Je venais d'écrire un code rapide et j'ai remarqué cette erreur de complier

  

L'utilisation de la variable d'itération dans une expression lambda peut avoir des résultats inattendus.
  Au lieu de cela, créez une variable locale dans la boucle et attribuez-lui la valeur de la variable d'itération.

Je sais ce que cela signifie et je peux facilement le réparer, ce n'est pas bien grave.
Mais je me demandais pourquoi c'est une mauvaise idée d'utiliser une variable d'itération dans un lambda?
Quels problèmes puis-je causer plus tard?

Était-ce utile?

La solution

Considérez ce code:

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'attendriez-vous que cela soit imprimé? La réponse évidente est 0 ... 9 - mais en réalité, elle en imprime 10, dix fois. C'est parce qu'il n'y a qu'une seule variable qui est capturée par tous les délégués. C'est ce genre de comportement qui est inattendu.

EDIT: Je viens de voir que vous parlez de VB.NET plutôt que de C #. Je pense que VB.NET a des règles encore plus compliquées, en raison de la façon dont les variables conservent leurs valeurs entre les itérations. Ce message de Jared Parsons donne des informations sur le type de difficultés rencontrées - bien qu’il soit de retour de 2007, le comportement réel a peut-être changé depuis.

Autres conseils

En supposant que vous vouliez dire C # ici.

Cela tient à la manière dont le compilateur implémente les fermetures. L'utilisation d'une variable d'itération peut causer un problème lors de l'accès à une fermeture modifiée (notez que j'ai dit «ne peut pas» causera un problème, car cela ne se produit parfois pas en fonction des autres éléments de la méthode. et parfois vous voulez réellement accéder à la fermeture modifiée).

Plus d'infos:

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

Encore plus d'informations:

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

Théorie des fermetures dans .NET

Variables locales: étendue et durée de vie (plus les fermetures) (Archivé en 2010)

(Soulignez moi)

  

Dans ce cas, nous utilisons une fermeture. Une fermeture est simplement une structure spéciale qui vit en dehors de la méthode et qui contient les variables locales auxquelles d'autres méthodes doivent faire référence. Lorsqu'une requête fait référence à une variable locale (ou à un paramètre), cette variable est capturée par la fermeture et toutes les références à cette variable sont redirigées vers la fermeture.

Lorsque vous réfléchissez au fonctionnement des fermetures dans .NET, je vous recommande de garder ces points à l’esprit, c’est ce que les concepteurs ont dû travailler lorsqu’ils implémentaient cette fonctionnalité:

  • Notez que " variable capture " et les expressions lambda ne sont pas une fonctionnalité IL, VB.NET (et C #) devait implémenter ces fonctionnalités à l’aide d’outils existants, ici des classes et des Delegate .
  • Autrement dit, les variables locales ne peuvent pas être persistées au-delà de leur portée. Le langage fait en sorte que il semble qu’il le peut, mais ce n’est pas une abstraction parfaite.
  • Les instances
  • Func (Of T) (c'est-à-dire, Délégué ) n'ont aucun moyen de stocker les paramètres qui y sont passés.
  • Bien que Func (Of T) stocke l'instance de la classe dont la méthode fait partie. C’est l’avenue utilisée par le framework .NET pour "se souvenir". paramètres passés dans les expressions lambda.

Eh bien, jetons un coup d'oeil!

Exemple de code:

Alors disons que vous avez écrit du code comme celui-ci:

' 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

Vous vous attendez peut-être à ce que le code s'imprime en 0,1,2,3, mais il imprime en réalité 4,4,4,4, car indexParameter a été "capturé". dans l'étendue de la portée de Sub VBDotNetSample () , et non dans l'étendue de la boucle For .

Exemple de code décompilé

Personnellement, je voulais vraiment savoir quel type de code le compilateur générait pour cela, alors je suis allé de l'avant et j'ai utilisé JetBrains DotPeek. J'ai pris le code généré par le compilateur et l'ai traduit à la main en VB.NET.

Commentaires et noms de variables mine. Le code a été légèrement simplifié d'une manière qui n'affecte pas le comportement du code.

<*>

Notez qu’il n’existe qu’une seule instance de closingHelperClass pour le corps entier de Sub CompilerGenerated , de sorte que la fonction ne peut en aucune manière imprimer le code intermédiaire. valeurs d'index de boucle de 0,1,2,3 (il n'y a pas de place pour stocker ces valeurs). Le code n’imprime que 4, la valeur d’index finale (après la boucle For ) quatre fois.

Notes de bas de page:

  • Il existe un "à partir de .NET 4.6.1" implicite. dans ce post, mais à mon avis, il est très peu probable que ces limitations changent radicalement; si vous trouvez une configuration dans laquelle vous ne pouvez pas reproduire ces résultats, laissez-moi un commentaire.

"Mais jrh pourquoi avez-vous posté une réponse tardive?"

  • Les pages liées dans cet article sont soit manquantes, soit en ruine.
  • Il n'y avait pas de réponse de vb.net sur cette question balisée vb.net. Au moment de la rédaction du présent document, il existe une réponse en C # (langue incorrecte) et une réponse essentiellement composée uniquement de liens (avec 3 liens inactifs).
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top