Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using the iterator variable of foreach loop in a lambda expression - why fails?

Tags:

Consider the following code:

public class MyClass {    public delegate string PrintHelloType(string greeting);       public void Execute()     {          Type[] types = new Type[] { typeof(string), typeof(float), typeof(int)};         List<PrintHelloType> helloMethods = new List<PrintHelloType>();          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;     }  ...  } 

After calling myClass.Execute(), the code prints the following unexpected response:

 Hi Int32 Hi Int32 Hi Int32   

Obviously, I would expect "Hi String", "Hi Single", "Hi Int32", but apparently it is not the case. Why the last element of the iterated array is being used in all the 3 methods instead of the appropriate one?

How would you rewrite the code to achieve the desired goal?

like image 551
user256890 Avatar asked Jul 02 '10 18:07

user256890


2 Answers

Welcome to the world of closures and captured variables :)

Eric Lippert has an in-depth explanation of this behaviour:

  • Closing over the loop variable considered harmful
  • Closing over the loop variable, part two

basically, it's the loop variable that is captured, not it's value. To get what you think you should get, do this:

foreach (var type in types) {    var newType = type;    var sayHello =              new PrintHelloType(greeting => SayGreetingToType(newType, greeting));    helloMethods.Add(sayHello); } 
like image 113
SWeko Avatar answered Oct 02 '22 09:10

SWeko


As a brief explanation that alludes to the blog postings that SWeko referenced, a lambda is capturing the variable, not the value. In a foreach loop, the variable is not unique on each iteration, the same variable is used for the duration of the loop (this is more obvious when you see the expansion the compiler performs on the foreach at compile time). As a result, you've captured the same variable during each iteration, and the variable (as of the last iteration) refers to the last element of your set.

Update: In newer versions of the language (beginning in C# 5), the loop variable is considered new with each iteration, so closing over it does not produce the same problem as it did in older versions (C# 4 and prior).

like image 26
Anthony Pegram Avatar answered Oct 02 '22 11:10

Anthony Pegram