2014-05-07 11 views
34

Lassen Sie mich im Voraus entschuldigen - ich bin wahrscheinlich Schlachter die Terminologie. Ich habe ein vages Verständnis davon, was eine Schließung ist, kann aber das Verhalten, das ich sehe, nicht erklären. Zumindest denke ich, dass es ein Schließungsproblem ist. Ich habe online gesucht, aber nicht die richtigen Keywords gefunden, um zu bekommen, was ich möchte.Warum sind einige Verschlüsse "freundlicher" als andere?

Genauer gesagt - ich habe zwei Blöcke Code, die wirklich ähnlich sind (zumindest für meine Augen). Erstens:

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++)); 
} 

Wenn Sie diesen Code ausführen, und rufen x1() und x2() Sie werden sehen, dass sie einen separaten ‚count‘ Wert zu halten.

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

Ausgänge:

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

Das macht Sinn für mich und stimmt mit den Erklärungen ich gelesen habe. Hinter den Kulissen wird für jeden Delegierten/jede Aktion eine Klasse erstellt und der Klasse wird ein Feld zugewiesen, in dem der Wert von "count" gespeichert wird. Ich ging ins Bett und fühlte mich schlau!

aber dann - ich versuchte, diesen sehr ähnlichen 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++); 

Und (wie der Kommentar sagt) das Verhalten ist hier völlig anders. x3() und x4() scheinen den gleichen Zählwert zu haben!

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

ich kann sehen, was passiert - aber ich weiß nicht wirklich, warum sie anders behandelt werden. In meinem Kopf - ich mochte dieses ursprüngliche Verhalten, das ich sah, aber das spätere Beispiel verwirrt mich. Ich hoffe das ergibt Sinn. Danke

+27

Ihr erstes Beispiel hatte zwei verschiedene 'int count'-Variablendeklarationen (aus den separaten Methodenaufrufen).Ihr zweites Beispiel teilt die _Same_Variablen-Deklaration. Ihr erstes Beispiel würde sich genauso verhalten wie das zweite Beispiel, bei dem "int count" ein Feld Ihres Hauptprogramms war. –

Antwort

50

Ihr erstes Beispiel hatte zwei verschiedene int count Variablendeklarationen (von den separaten Methodenaufrufen). Ihr zweites Beispiel teilt die selbe Variablendeklaration.

Ihr erstes Beispiel das gleiche wie das zweite Beispiel verhalten hatte int count ein Feld des Hauptprogramms gewesen:

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++)); 
} 

Diese Ausgänge:

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

Sie es ohne den ternären Operator vereinfachen ebenso:

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

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

Welche Ausgänge:

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

Das Hauptproblem ist, dass ein lokal Variable in einer Methode deklarierte (in Ihrem Fall int count = 0;) für einzigartig ist, dass des Methodenaufrufes, dann, wenn der Lambda-Delegat erstellt wird, die jeweils bewirbt Schließung um sein eigenes einzigartiges count Variable:

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

Der Unterschied ist, dass in einem Beispiel Sie einen Delegierten haben, der andere Sie zwei haben.

Da die Zählvariable lokal ist, wird sie jedes Mal neu generiert, wenn Sie den Anruf tätigen. Da nur ein Delegat verwendet wird (aufgrund der Ternärstruktur), erhält jeder Delegat eine andere Kopie der Variablen. Im anderen Beispiel erhalten beide Delegaten die gleiche Variable.

Ein ternärer Operator gibt nur eines der beiden Argumente zurück, so dass die Schließung wie erwartet funktioniert. Im zweiten Beispiel erstellen Sie zwei Closures, die dieselbe "übergeordnete" count-Variable teilen und ein unterschiedliches Ergebnis ergeben.

Es könnte ein bisschen klarer, wenn man es so aussehen (dies entspricht Code in Ihrer ersten Probe):

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 
} 

Offensichtlich gibt nur einen Verschluss hier, und Ihre andere Probe offensichtlich erzeugte hat zwei.

+4

Äh, ich glaube nicht, dass der ternäre Operator etwas damit zu tun hat; Es sind die zwei Methodenaufrufe, die zwei lokale Variablen erstellen. Wenn Sie den ternären Operator entfernen, haben Sie das gleiche Ergebnis (außer dass es jedes Mal "1" bedeutet), aber das "count" Verhalten wäre dasselbe. –

+0

@ChrisSinclair, das ist wahr. Die Verwirrung scheint sich um die Tatsache zu drehen, dass es aussieht, als ob es auch zwei Delegierte gäbe. Ich werde diese Informationen aktualisieren, aber danke! – BradleyDotNET

27

Verschluss fängt eine Variable.

A lokale Variable erstellt wird, wenn eine Methode indem sie aktiviert ist genannt. (Es gibt andere Dinge, die lokale Variablen erzeugen, aber lass uns das jetzt ignorieren.)

In deinem ersten Beispiel hast du zwei Aktivierungen von GetWorker und daher werden zwei völlig unabhängige Variablen namens count erstellt. Jeder wird unabhängig erfasst.

In Ihrem zweiten Beispiel, das Sie leider nicht alle zeigen, haben Sie eine einzige Aktivierung und zwei Schließungen. Die Schließungen teilen sich die Variable.

Hier ist ein Weg, um darüber nachzudenken, die helfen könnten:

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. 
} 

Hat es einen Sinn für Sie, warum es im ersten Beispiel sind zwei Variablen count genannt, die nicht geteilt durch mehrere Objekte, und in der Sekunde gibt es nur eine, die von mehreren Objekten geteilt wird?

+0

"Es gibt andere Dinge, die lokale Variablen erstellen, aber lass uns das jetzt ignorieren." Ich betrachte das aus einer MSIL-Perspektive, was könnte sonst noch eine lokale Variable erzeugen? – riki

+2

@Felheart: Ich habe etwas ungenau gesprochen; eher hätte ich sagen sollen, dass es Programmelemente gibt, die keine lokalen Variablen sind, die aber erfasst werden können. Formale Parameter mit Übergabewert sind Variablen, die erfasst werden können. Auch der 'this'-Wert ist keine Variable, die übergeben wird, kann aber erfasst werden. In gewissem Sinne wird das Argument type auch dann erfasst, wenn eine generische Methode ein Lambda enthält, und das ist keine Art von Variable. –

2

Eine weitere Alternative (von dem, was vielleicht für Sie gesucht):

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

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

Dann:

var x = GetWorker(); 

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

Oder vielleicht:

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

Oder vielleicht mit etwas 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(); 
} 
Verwandte Themen