34 votes

Pourquoi certaines fermetures sont-elles plus "amicales" que d'autres?

Permettez-moi de m'excuser à l'avance - je suis probablement le dépeçage de la terminologie. J'ai une vague idée de ce qu'est une fermeture, mais ne peut pas expliquer le comportement que je vois. Au moins, je pense que c'est un problème la fermeture. J'ai cherché en ligne, mais je n'ai pas trouvé les bons mots-clés pour obtenir ce que je veux.

Plus précisément - j'ai deux blocs de code sont VRAIMENT SIMILAIRES (au moins à mes yeux). D'abord:

static void Main(string[] args)
{
    Action x1 = GetWorker(0);
    Action x2 = GetWorker(1);
}

static Action GetWorker(int k)
{
    int count = 0;

    // Each Action delegate has it's own 'captured' count variable
    return k == 0 ? (Action)(() => Console.WriteLine("Working 1 - {0}",count++))
                  : (Action)(() => Console.WriteLine("Working 2 - {0}",count++));
}

Si vous exécutez ce code et invoquer x1() et x2() vous verrez qu'ils maintiennent un distinct "comte" de la valeur.

    foreach(var i in Enumerable.Range(0,4))
    {
        x1(); x2(); 
    }

Sorties:

Working 1 - 0
Working 2 - 0
Working 1 - 1
Working 2 - 1
Working 1 - 2
Working 2 - 2
Working 1 - 3
Working 2 - 3

Qui fait sens pour moi et les matchs les explications que j'ai lu. Les coulisses d'une classe est créée pour chaque délégué/action et la classe est un champ pour stocker la valeur de "compter". Je suis allé au lit sentiment smart!

MAIS PUIS - je essayé ce très semblable code:

    // x3 and x4 *share* the same 'captured' count variable
    Action x3 = () => Console.WriteLine("Working 3 - {0}", count++);
    Action x4 = () => Console.WriteLine("Working 4 - {0}", count++);

Et, comme le commentaire dit), le comportement est complètement différent ici. x3() et x4() semblent avoir la MÊME valeur du nombre de!

Working 3 - 0
Working 4 - 1
Working 3 - 2
Working 4 - 3
Working 3 - 4
Working 4 - 5
Working 3 - 6
Working 4 - 7

Je peux voir ce qui se passe, mais je ne suis pas vraiment pourquoi ils sont traités différemment. Dans ma tête - j'ai aimé que l'origine du comportement, j'ai été voir, mais ce dernier exemple me confond. J'espère qu'un sens. Merci

50voto

Chris Sinclair Points 14829

Votre premier exemple eu deux int count des déclarations de variables (à partir de la méthode séparée des appels). Votre deuxième exemple est le partage de la même déclaration de variable.

Votre premier exemple, comportent le même que le deuxième exemple a int count été un champ de votre programme principal:

static int count = 0;

static Action GetWorker(int k)
{
    return k == 0 ? (Action)(() => Console.WriteLine("Working 1 - {0}",count++))
                  : (Action)(() => Console.WriteLine("Working 2 - {0}",count++));
}

Ce sorties:

Working 1 - 0
Working 2 - 1
Working 1 - 2
Working 2 - 3
Working 1 - 4
Working 2 - 5
Working 1 - 6
Working 2 - 7

Vous pouvez simplifier sans l'opérateur ternaire ainsi:

static Action GetWorker(int k)
{
    int count = 0;

    return (Action)(() => Console.WriteLine("Working {0} - {1}",k,count++));
}

Sorties:

Working 1 - 0
Working 2 - 0
Working 1 - 1
Working 2 - 1
Working 1 - 2
Working 2 - 2
Working 1 - 3
Working 2 - 3

Le principal problème est qu'une variable locale déclarée dans une méthode (dans votre cas int count = 0;) est unique pour que l'invocation de la méthode, puis, quand le lambda délégué est créé, chacun est l'application de clôture autour de sa propre count variable:

Action x1 = GetWorker(0); //gets a count
Action x2 = GetWorker(1); //gets a new, different count

27voto

Eric Lippert Points 300275

Une fermeture à la capture d'une variable.

Une variable locale est créée lorsqu'une méthode est activé par être appelé. (Il y a d'autres choses que de créer des variables locales, mais passons pour l'instant.)

Dans votre premier exemple, vous avez deux activations de GetWorker et, par conséquent, deux variables indépendantes nommées count sont créés. Chacun d'eux est capturé de façon indépendante.

Dans ton deuxième exemple, qui, malheureusement, vous ne montrez pas toutes, vous avez une activation unique et deux fermetures. Les fermetures de la part de la variable.

Voici une façon de penser qui pourraient vous aider:

class Counter { public int count; }
...
Counter Example1()
{
    return new Counter();
}
...
Counter c1 = Example1();
Counter c2 = Example1();
c1.count += 1;
c2.count += 2;
// c1.count and c2.count are different.

Vs

void Example2()
{
    Counter c = new Counter();
    Counter x3 = c; 
    Counter x4 = c;
    x3.count += 1;
    x4.count += 2;
    // x3.count and x4.count are the same.
}

Est-il judicieux de vous expliquer pourquoi dans le premier exemple il y a deux variables appelées count qui ne sont pas partagées par plusieurs objets, et dans le deuxième il y a un seul, partagé par plusieurs objets?

6voto

BradleyDotNET Points 12000

La différence est que, dans un exemple, vous avez un délégué, les autres vous avez deux.

Depuis le comptage variable est locale, il est régénéré à chaque fois que vous faites un appel. Depuis un seul délégué est utilisé (en raison de la ternaire) chaque délégué reçoit une autre copie de la variable. Dans l'autre exemple, les deux délégués de la même variable.

Un opérateur ternaire retourne uniquement l'un de ses deux arguments, de sorte que la fermeture fonctionne comme prévu. Dans le deuxième exemple, vous créez deux fermetures qui partagent la même "mère" nombre variable, donnant le résultat différent.

Il pourrait être un peu plus clair, si vous regardez cela de cette façon (c'est le code équivalent à votre premier échantillon):

static Action GetWorker(int k)
{
    int count = 0;
    Action returnDelegate

    // Each Action delegate has it's own 'captured' count variable
    if (k == 0)
         returnDelegate = (Action)(() => Console.WriteLine("Working 1 - {0}",count++));
    else
         returnDelegate = (Action)(() => Console.WriteLine("Working 2 - {0}",count++));

    return returnDelegate
}

Clairement il y a une seule fermeture généré ici, et votre autre échantillon a évidemment deux.

2voto

leppie Points 67289

Une autre alternative (de ce que vous cherchiez peut-être):

 static Action<int> GetWorker()
{
    int count = 0;

    return k => k == 0 ? 
             Console.WriteLine("Working 1 - {0}",count++) : 
             Console.WriteLine("Working 2 - {0}",count++);
}
 

Ensuite:

 var x = GetWorker();

foreach(var i in Enumerable.Range(0,4))
{
    x(0); x(1);
}    
 

Ou peut-être:

 var y = GetWorker();
// and now we refer to the same closure
Action x1 = () => y(0);
Action x2 = () => y(1);

foreach(var i in Enumerable.Range(0,4))
{
    x1(); x2(); 
}
 

Ou peut-être avec du curry:

 var f = GetWorker();
Func<int, Action> GetSameWorker = k => () => f(k);

//  k => () => GetWorker(k) will not work

Action z1 = GetSameWorker(0);
Action z2 = GetSameWorker(1);    

foreach(var i in Enumerable.Range(0,4))
{
    z1(); z2(); 
}
 

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