Usando la variabile iteratore del ciclo foreach in un’espressione lambda – perché fallisce?

Considera il seguente codice:

public class MyClass { public delegate string PrintHelloType(string greeting); public void Execute() { Type[] types = new Type[] { typeof(string), typeof(float), typeof(int)}; List helloMethods = new List(); foreach (var type in types) { var sayHello = new PrintHelloType(greeting => SayGreetingToType(type, greeting)); helloMethods.Add(sayHello); } foreach (var helloMethod in helloMethods) { Console.WriteLine(helloMethod("Hi")); } } public string SayGreetingToType(Type type, string greetingText) { return greetingText + " " + type.Name; } ... } 

Dopo aver chiamato myClass.Execute() , il codice stampa la seguente risposta inaspettata:

 Ciao Int32
 Ciao Int32
 Ciao Int32  

Ovviamente, mi aspetterei "Hi String" , "Hi Single" , "Hi Int32" , ma a quanto pare non è il caso. Perché l’ultimo elemento dell’array iterato viene utilizzato in tutti e 3 i metodi anziché quello appropriato?

Come riscriverebbe il codice per raggiungere l’objective desiderato?

Benvenuti nel mondo delle chiusure e delle variabili catturate 🙂

Eric Lippert ha una spiegazione approfondita di questo comportamento:

  • Chiusura sulla variabile del ciclo considerata dannosa
  • Chiusura sulla variabile del ciclo, seconda parte

fondamentalmente, è la variabile di loop che viene catturata, non il suo valore. Per ottenere ciò che pensi di dover ottenere, fai questo:

 foreach (var type in types) { var newType = type; var sayHello = new PrintHelloType(greeting => SayGreetingToType(newType, greeting)); helloMethods.Add(sayHello); } 

Come una breve spiegazione che allude ai post sui blog a cui fa riferimento SWeko, un lambda sta acquisendo la variabile , non il valore . In un ciclo foreach, la variabile non è univoca su ogni iterazione, la stessa variabile viene utilizzata per la durata del ciclo (questo è più ovvio quando si vede l’espansione che il compilatore esegue sul foreach in fase di compilazione). Di conseguenza, hai catturato la stessa variabile durante ogni iterazione e la variabile (a partire dall’ultima iterazione) si riferisce all’ultimo elemento del tuo set.

Aggiornamento: nelle versioni più recenti del linguaggio (a partire dal C # 5), la variabile di ciclo è considerata nuova ad ogni iterazione, quindi chiuderla non produce lo stesso problema delle versioni precedenti (C # 4 e precedenti).

Puoi sistemarlo introducendo una variabile aggiuntiva:

 ... foreach (var type in types) { var t = type; var sayHello = new PrintHelloType(greeting => SayGreetingToType(t, greeting)); helloMethods.Add(sayHello); } ....