In this post I want to discuss with you the importance of realizing how lambdas work (and why you should care). Let's dive right in with some code.
Given the following code snippet, what would you expect the output to be (no cheating :P)?
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action();
}
Would you believe me if I told you this is the output you get?
10
10
10
10
10
10
10
10
10
10
At first glance, one might expect this output:
0
1
2
3
4
5
6
7
8
9
But all tens are output instead. Why does this happen? Let's crank open Reflector and find out why...
The first thing you'll notice is that the compiler has created a helper class to enable the closure we have. This helper class created by the compiler contains a local variable that we use to iterate over and a method that our delegate is contained within. This helper class is especially interesting because it is where the magic happens.
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
// Fields
public int i;
// Methods
public void <Main>b__0()
{
Console.WriteLine(this.i);
}
}
Rather than diving into MSIL, let's look at some pseudo-code of what the compiler _actually_ executes with the original code above (based on the generated MSIL):
var actions = new List<Action>();
<>c__DisplayClass2 localFrame = new <>c__DisplayClass2();
for (localFrame.i = 0; localFrame.i < 10; localFrame.i++)
{
actions.Add(localFrame.<Main>b__0);
}
foreach (var action in actions)
{
action();
}
Perhaps now you can see where the problem is. The "problem" in our original code exists because of the scope that our closures are defined. The local index from our for loop is now stored in our helper class <>c__DisplayClass2. And the code that is executed by the action is contained within the compiler generated <Main>b__0() method now. So the Console.WriteLine() method now uses the local variable from <>c__DisplayClass2 when it is executed.
So while we are looping through building all our actions, we are also incrementing the property i in localFrame (an instance of <>c__DisplayClass2). Then at the end when we are actually executing the actions, the <Main>b__0() is called and uses the local i property (which by this time, has already been incremented to 10 from our loop). And that's why every action we execute prints "10" instead of the 0 through 9 like we expected.
So, why do you need to know how these work? Take the following code that outputs items from an array of strings:
var actions = new List<Action>();
string[] urls =
{
"http://www.url.com",
"http://www.someurl.com",
"http://www.someotherurl.com",
"http://www.yetanotherurl.com"
};
for (int i = 0; i < urls.Length; i++)
{
actions.Add(() => Console.WriteLine(urls[i]));
}
foreach (var action in actions)
{
action();
}
This code looks pretty innocuous. Our bounds are protected, and we just index into our array to output a string. But, is that what we're really doing? Remember how closures work from above. The actual thing that happens when I run this code is this:
Confusing
Interesting. Even though our index variable should only even be less than the length of our url array, an exception is thrown because the index variable is actually equal to the length of our url array (and hence outside of the bounds thanks to 0-based indices).
Well, that wasn't what we were probably expecting. But now that we are having this "problem", what is the easiest way to resolve it? Remember that the problem is happening because of the scope of the variable within our closure. So to fix this, we can essentially declare a temporary variable that is unique in scope to this specific iteration through our array:
for (int i = 0; i < urls.Length; i++)
{
string localUrl = urls[i];
actions.Add(() => Console.WriteLine(localUrl));
}
And now the code is fixed.
Understanding how lambdas work is especially important when you start developing with a library that leverages lambdas heavily like LINQ does, or Parallel Extensions to the .NET Framework. And don't worry, even those people that know how lambdas work occasionally get bitten by this behavior.
Enjoy the coding, folks!