Variabile catturata in un ciclo in C #

Ho incontrato un problema interessante su C #. Ho il codice come sotto.

List<Func> actions = new List<Func>(); int variable = 0; while (variable  variable * 2); ++ variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Mi aspetto che emetta 0, 2, 4, 6, 8. Tuttavia, in realtà emette cinque 10 secondi.

Sembra che sia dovuto a tutte le azioni che fanno riferimento a una variabile catturata. Di conseguenza, quando vengono richiamati, hanno tutti lo stesso risultato.

C’è un modo per aggirare questo limite per fare in modo che ogni istanza di azione abbia la propria variabile acquisita?

Sì, prendi una copia della variabile all’interno del loop:

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

Puoi pensarci come se il compilatore C # crea una “nuova” variabile locale ogni volta che colpisce la dichiarazione della variabile. Infatti creerà appropriati nuovi oggetti di chiusura, e diventa complicato (in termini di implementazione) se si fa riferimento a variabili in più ambiti, ma funziona 🙂

Si noti che un’occorrenza più comune di questo problema è l’utilizzo for foreach o foreach :

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

Vedi la sezione 7.14.4.2 della specifica C # 3.0 per maggiori dettagli, e il mio articolo sulle chiusure ha anche altri esempi.

Credo che ciò che stai vivendo sia qualcosa noto come Closure http://en.wikipedia.org/wiki/Closure_(computer_science) . Il tuo lamba ha un riferimento a una variabile che è scopata al di fuori della funzione stessa. Il tuo lamba non viene interpretato finché non lo si richiama e una volta ottenuto otterrà il valore che la variabile ha al momento dell’esecuzione.

Dietro le quinte, il compilatore sta generando una class che rappresenta la chiusura per la tua chiamata al metodo. Usa quella singola istanza della class di chiusura per ogni iterazione del ciclo. Il codice ha un aspetto simile a questo, il che rende più facile capire perché l’errore si verifica:

 void Main() { List> actions = new List>(); int variable = 0; var closure = new CompilerGeneratedClosure(); Func anonymousMethodAction = null; while (closure.variable < 5) { if(anonymousMethodAction == null) anonymousMethodAction = new Func(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; } } 

Questo non è in realtà il codice compilato dal tuo campione, ma ho esaminato il mio codice e questo sembra molto simile a ciò che il compilatore potrebbe effettivamente generare.

Il modo per aggirare questo è quello di memorizzare il valore necessario in una variabile proxy, e ottenere tale variabile viene catturata.

IE

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

Sì, è necessario modificare la variable all’interno del ciclo e passarla al lambda in questo modo:

 List> actions = new List>(); 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(); 

La stessa situazione sta accadendo nel multi-threading (C #, .NET 4.0).

Vedere il seguente codice:

Scopo è stampare 1,2,3,4,5 nell’ordine.

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

L’output è interessante! (Potrebbe essere come 21334 …)

L’unica soluzione è usare le variabili locali.

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

Questo non ha nulla a che fare con i loop.

Questo comportamento viene triggersto perché si utilizza una espressione lambda () => variable * 2 cui la variable scope esterna non è effettivamente definita nell’ambito interno di lambda.

Le espressioni Lambda (in C # 3 +, così come i metodi anonimi in C # 2) continuano a creare metodi reali. Passare le variabili a questi metodi comporta alcuni dilemmi (passaggio per valore? Passaggio per riferimento? C # per riferimento – ma questo apre un altro problema in cui il riferimento può sopravvivere alla variabile attuale). Che cosa fa C # per risolvere tutti questi dilemmi è creare una nuova class helper (“chiusura”) con campi corrispondenti alle variabili locali utilizzate nelle espressioni lambda e metodi corrispondenti ai metodi lambda effettivi. Qualsiasi modifica alla variable nel tuo codice viene in realtà tradotta per cambiare in quella ClosureClass.variable

Quindi il tuo ciclo while continua ad aggiornare ClosureClass.variable fino a raggiungere 10, quindi tu per cicli esegue le azioni, che operano tutte sullo stesso ClosureClass.variable .

Per ottenere il risultato previsto, è necessario creare una separazione tra la variabile di loop e la variabile che viene chiusa. Puoi farlo introducendo un’altra variabile, cioè:

 List> actions = new List>(); int variable = 0; while (variable < 5) { var t = variable; // now t will be closured (ie replaced by a field in the new class) actions.Add(() => t * 2); ++variable; // changing variable won't affect the closured variable t } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Puoi anche spostare la chiusura su un altro metodo per creare questa separazione:

 List> actions = new List>(); int variable = 0; while (variable < 5) { actions.Add(Mult(variable)); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

È ansible implementare Mult come espressione lambda (chiusura implicita)

 static Func Mult(int i) { return () => i * 2; } 

o con una class di helper effettiva:

 public class Helper { public int _i; public Helper(int i) { _i = i; } public int Method() { return _i * 2; } } static Func Mult(int i) { Helper help = new Helper(i); return help.Method; } 

In ogni caso, le "chiusure" NON sono un concetto correlato ai loop , ma piuttosto ai metodi anonimi / espressioni lambda che usano le variabili scope locali - sebbene qualche incauto uso di loop dimostri trappole di chiusura.