276 votes

Variable capturée dans une boucle en C #

J'ai rencontré un numéro intéressant sur C #. J'ai un code comme ci-dessous.

 List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}
 

Je m'attends à ce qu'il produise 0, 2, 4, 6, 8. Cependant, il produit en fait cinq 10.

Il semble que cela soit dû à toutes les actions faisant référence à une variable capturée. En conséquence, lorsqu'ils sont appelés, ils ont tous le même résultat.

Est-il possible de contourner cette limite pour que chaque instance d'action ait sa propre variable capturée?

254voto

Jon Skeet Points 692016

Oui - une copie de la variable à l'intérieur de la boucle:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

Vous pouvez y penser comme si le compilateur C# crée une "nouvelle" variable locale à chaque fois qu'il frappe la déclaration de la variable. En fait, il va créer de nouvelles fermeture d'objets, et il devient compliqué (en termes de mise en œuvre), si vous vous référez à des variables dans plusieurs champs d'application, mais il fonctionne :)

Notez que l'un des plus courante de ce problème est l'utilisation de for ou foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

Voir la section 7.14.4.2 de C# 3.0 pour plus de détails de cette, et mon article sur les fermetures a plus d'exemples de trop.

27voto

TheCodeJunkie Points 4074

Je crois que ce que vous vivez est quelque chose appelé Closure http://en.wikipedia.org/wiki/Closure_(computer_science) . Votre lamba fait référence à une variable qui se situe en dehors de la fonction elle-même. Votre lamba n'est pas interprétée tant que vous ne l'appelez pas et une fois que cela sera fait, elle obtiendra la valeur de la variable au moment de l'exécution.

16voto

Gerrard Lindsay Points 189

Dans les coulisses, le compilateur génère une classe qui représente la fermeture de votre appel de méthode. Il utilise cette instance unique de la classe de fermeture pour chaque itération de la boucle. Le code ressemble à ceci, ce qui permet de voir plus facilement pourquoi le bogue se produit:

             void Main()
            {
                List<Func<int>> actions = new List<Func<int>>();

                int variable = 0;

                var closure = new CompilerGeneratedClosure();

                Func<int> anonymousMethodAction = null;

                while (closure.variable < 5)
                {
                    if(anonymousMethodAction == null)
                        anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

                    //we're re-adding the same function 
                    actions.Add(anonymousMethodAction);

                    ++closure.variable;
                }

                foreach (var act in actions)
                {
                    Console.WriteLine(act.Invoke());
                }
            }

            class CompilerGeneratedClosure
            {
                public int variable;

                public int YourAnonymousMethod()
                {
                    return this.variable * 2;
                }
            }
 

Ce n'est pas réellement le code compilé de votre exemple, mais j'ai examiné mon propre code et cela ressemble beaucoup à ce que le compilateur générerait réellement.

7voto

cfeduke Points 13153

Oui, vous devez définir variable dans la boucle et le transmettre au lambda de cette façon:

 List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();
 

6voto

Sunil Points 1375

La même situation se produit dans le multi-threading (C #, .NET 4.0).

Voir le code suivant:

Le but est d'imprimer 1,2,3,4,5 dans l'ordre.

 for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}
 

La sortie est intéressante! (Ça pourrait être comme 21334 ...)

La seule solution consiste à utiliser des variables locales.

 for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X