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

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?

Answers


Welcome to the world of closures and captured variables :)

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

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);
}

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).


You can fix it by introducing additional variable:

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

Need Your Help

SSDT-BI SSIS in x64?

sql-server visual-studio-2012 ssis sql-server-data-tools ssdt-bi

I run Visual Studio 2012 and I'm new to SSIS. I attempted to install SSDT-BI (June 2013 version) in order to research SSIS, but I was unable to complete the install with my default instance because...

Comparing certificates using Powershell

powershell x509certificate

I'm working on a disaster recovery project and I am recommending as part of the plan to do regular audits of the primary and secondary sites. One of the audit tasks is to make sure that the second...