2012-06-15 8 views
14

Ho appena incontrato il seguente comportamento:Come posso acquisire il valore di una variabile esterna all'interno di un'espressione lambda?

for (var i = 0; i < 50; ++i) { 
    Task.Factory.StartNew(() => { 
     Debug.Print("Error: " + i.ToString()); 
    }); 
} 

si tradurrà in una serie di "Errore: x", dove la maggior parte delle x sono uguali a 50.

Allo stesso modo:

var a = "Before"; 
var task = new Task(() => Debug.Print("Using value: " + a)); 
a = "After"; 
task.Start(); 

darà come risultato "Uso del valore: dopo".

Ciò significa chiaramente che la concatenazione nell'espressione lambda non si verifica immediatamente. Come è possibile utilizzare una copia della variabile esterna nell'espressione lambda, nel momento in cui viene dichiarata l'espressione? Quanto segue non funzionerà meglio (che non è necessariamente incoerente, lo ammetto):

var a = "Before"; 
var task = new Task(() => { 
    var a2 = a; 
    Debug.Print("Using value: " + a2); 
}); 
a = "After"; 
task.Start(); 
+0

perché dovrebbero? Sono asincroni comunque. – Vlad

+0

Possibile duplicato "C# Variabile acquisita in loop" http://stackoverflow.com/questions/271440/c-sharp-captured-variable-in-loop –

+0

IMHO, si finisce per fare 2 domande qui - appare il "reale" essere nel titolo (come acquisire il valore, in modo tale che l'attività venga eseguita sul valore al tempo del ciclo), ma il corpo della domanda sembra focalizzarsi su "perché queste cose producono valori imprevisti" (l'effetto di la chiusura cattura il significato che fanno tutti riferimento alla stessa variabile). Quindi, si finisce con la maggior parte delle risposte che spiegano il comportamento invece di rispondere alla tua domanda "reale" (AFAICT :) –

risposta

23

Questo ha più a che fare con lambda di filettatura. Un lambda cattura il riferimento a una variabile, non il valore della variabile. Ciò significa che quando si tenta di utilizzare i nel proprio codice, il suo valore sarà quello memorizzato nello i ultimo.

Per evitare ciò, è necessario copiare il valore della variabile su una variabile locale all'avvio del lambda. Il problema è che l'avvio di un'attività ha un sovraccarico e la prima copia può essere eseguita solo al termine del ciclo. Il seguente codice anche fallire

for (var i = 0; i < 50; ++i) { 
    Task.Factory.StartNew(() => { 
     var i1=i; 
     Debug.Print("Error: " + i1.ToString()); 
    }); 
} 

Come ha fatto notare James Manning, è possibile aggiungere una variabile locale al loop e copiare l'indice del ciclo lì. In questo modo stai creando 50 variabili diverse per mantenere il valore della variabile di loop, ma almeno ottieni il risultato atteso. Il problema è che ottieni un sacco di allocazioni aggiuntive.

for (var i = 0; i < 50; ++i) { 
    var i1=i; 
    Task.Factory.StartNew(() => { 
     Debug.Print("Error: " + i1.ToString()); 
    }); 
} 

La soluzione migliore è passare il parametro ciclo come un parametro di stato:

for (var i = 0; i < 50; ++i) { 
    Task.Factory.StartNew(o => { 
     var i1=(int)o; 
     Debug.Print("Error: " + i1.ToString()); 
    }, i); 
} 

con uno stato risultati dei parametri in un minor numero allocazioni. Guardando il codice decompilato:

  • il secondo frammento di creerà 50 chiusure e 50 delegati
  • il terzo snippet creerà 50 interi in scatola, ma solo un singolo delegato
+1

La situazione è abbastanza nota, è descritta qui: http: //blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx –

+0

La seconda parte funziona, ma non la prima, quindi non posso ancora considerarla come una soluzione accettata. –

+1

Per il primo ciclo, la correzione 'right' (AFAICT) consiste nel fare var i1 = i; all'interno del ciclo ma prima di Task.Factory.StartNew. Con quel cambiamento, ogni chiusura farà riferimento alla propria variabile separata e otterrai il giusto effetto. Il parametro di stato evita la necessità della chiusura, tuttavia, quindi sicuramente più efficiente, ma non necessario se si desidera solo il comportamento corretto. –

4

Questo perché si esegue il codice in un nuovo thread e il thread principale va subito a modificare la variabile. Se l'espressione lambda è stata eseguita immediatamente, l'intero punto di utilizzo di un'attività verrà perso.

Il thread non ottiene la propria copia della variabile al momento della creazione dell'attività, tutte le attività utilizzano la stessa variabile (che in realtà è memorizzata nella chiusura per il metodo, non è una variabile locale).

3

Le espressioni lambda non catturano il valore della variabile esterna ma un riferimento ad esso. Questo è il motivo per cui vedi 50 o After nelle tue attività.

Per risolvere questo creare prima dell'espressione lambda una copia di esso per catturarlo per valore.

Questo sfortunato comportamento verrà risolto dal compilatore C# con .NET 4.5 fino ad allora è necessario vivere con questa stranezza.

Esempio:

List<Action> acc = new List<Action>(); 
    for (int i = 0; i < 10; i++) 
    { 
     int tmp = i; 
     acc.Add(() => { Console.WriteLine(tmp); }); 
    } 

    acc.ForEach(x => x()); 
+0

Vuoi dire che la creazione di una copia nell'espressione lambda funzionerà? Al momento non funziona: utilizzando var a2 = a; Logging.Print ("Utilizzo del valore:" + a2); ancora retruns "Using value: After". –

+0

Siamo spiacenti. È necessario posizionare la copia all'esterno del lambda per farlo funzionare. –

1

lambda espressioni sono, per definizione, pigramente valutato in modo che non saranno valutati fino a quando effettivamente chiamato. Nel tuo caso dall'esecuzione dell'attività. Se chiudi un locale nella tua espressione lambda, lo stato del locale al momento dell'esecuzione verrà riflesso. Che è quello che vedi Puoi approfittare di questo. Per esempio. il vostro ciclo for davvero non hanno bisogno di un nuovo lambda per ogni iterazione assumendo per il bene di questo esempio che il risultato descritto era quello che intendeva si potrebbe scrivere

var i =0; 
Action<int> action =() => Debug.Print("Error: " + i); 
for(;i<50;+i){ 
    Task.Factory.StartNew(action); 
} 

d'altra parte, se si voleva che in realtà stampato "Error: 1"..."Error 50" è possibile modificare il precedente per

var i =0; 
Func<Action<int>> action = (x) => { return() => Debug.Print("Error: " + x);} 
for(;i<50;+i){ 
    Task.Factory.StartNew(action(i)); 
} 

i primi chiude sopra i e utilizzerà lo stato della i al momento l'azione viene eseguita e lo stato è spesso andando ad essere lo stato dopo la fine del ciclo. In quest'ultimo caso, i viene valutato con entusiasmo perché viene passato come argomento a una funzione. Questa funzione restituisce quindi un Action<int> che viene passato a StartNew.

Quindi la decisione di progettazione rende possibile sia la valutazione pigramente che la valutazione impaziente. Lazily perché i locali sono chiusi sopra e avidamente perché è possibile forzare locali da eseguire passandoli come argomento o come illustrato di seguito dichiara un altro locale con una portata minore

for (var i = 0; i < 50; ++i) { 
    var j = i; 
    Task.Factory.StartNew(() => Debug.Print("Error: " + j)); 
} 

Tutto quanto sopra è generale per lambda. Nel caso specifico di StartNew ci sia effettivamente un sovraccarico che fa quello che il secondo esempio lo fa in modo che può essere semplificata

var i =0; 
Action<object> action = (x) => Debug.Print("Error: " + x);} 
for(;i<50;+i){ 
    Task.Factory.StartNew(action,i); 
} 
+0

BTW, che lambda può essere semplificato in 'x =>() => Debug.Print (" Errore: "+ x)'. – svick

Problemi correlati