Lambda variable capture in loop – cosa succede qui?

Sto cercando di capire, cosa succede qui? Che tipo di codice produce il compilatore?

public static void vc() { var listActions = new List(); foreach (int i in Enumerable.Range(1, 10)) { listActions.Add(() => Console.WriteLine(i)); } foreach (Action action in listActions) { action(); } } static void Main(string[] args) { vc(); } 

uscita: 10 10 .. 10

In base a ciò , verrà creata una nuova istanza di ActionHelper per ogni iterazione. In tal caso, suppongo che dovrebbe stampare 1..10. Qualcuno può darmi qualche pseudo-codice di ciò che il compilatore sta facendo qui?

Grazie.

In questa linea

  listActions.Add(() => Console.WriteLine(i)); 

la variabile i , viene catturata o, se lo desideri, creata un puntatore alla posizione di memoria di quella variabile. Ciò significa che ogni delegato ha un puntatore a quella posizione di memoria. Dopo questa esecuzione del ciclo:

 foreach (int i in Enumerable.Range(1, 10)) { listActions.Add(() => Console.WriteLine(i)); } 

per ovvi motivi i 10 , quindi il contenuto di memoria che puntano tutti i puntatori presenti Action nelle Action , diventa 10.

In altre parole, i sono catturato.

A proposito, dovresti notare che, secondo Eric Lippert, questo comportamento “strano” sarebbe stato risolto in C# 5.0 .

Quindi in C# 5.0 tuo programma dovrebbe stampare come previsto :

 1,2,3,4,5...10 

MODIFICARE:

Non riesci a trovare il post di Eric Lippert sull’argomento, ma eccone un altro:

Chiusura in un loop rivisitato

Che tipo di codice produce il compilatore?

Sembra che ti aspetti questo:

 foreach (int temp in Enumerable.Range(1, 10)) { int i = temp; listActions.Add(() => Console.WriteLine(i)); } 

Questo utilizza un diverso varaible per ogni iterazione, e quindi il valore della variabile nel momento in cui viene creata la lambda verrà catturato. In effetti, è ansible utilizzare questo codice esatto e ottenere il risultato desiderato.

Ma ciò che effettivamente fa il compilatore è qualcosa di più vicino a questo:

 int i; foreach (i in Enumerable.Range(1, 10)) { listActions.Add(() => Console.WriteLine(i)); } 

Questo rende chiaro che stai catturando la stessa variabile su ogni iterazione. Quando in seguito andate ed eseguite effettivamente quel codice, si riferiscono tutti allo stesso valore che è già stato incrementato fino a 10.

Questo non è un bug nel compilatore o nel runtime … è stata una decisione consapevole da parte del team di progettazione della lingua. Tuttavia, a causa della confusione su come funziona, hanno invertito questa decisione e hanno deciso di rischiare un cambiamento inaspettato per farlo funzionare più come ci si aspetta per C # 5 .

In sostanza, ciò che sta accadendo è che il compilatore sta dichiarando il tuo int i fuori dal ciclo e quindi non sta creando un nuovo valore per ogni azione, semplicemente facendo riferimento allo stesso ogni volta. Nel momento in cui esegui il codice i è 10 e quindi stampa 10 molto.