Managed World

Techno-babble from yet another babbler RSS 2.0
# Thursday, June 12, 2008

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!

Posted in C# | Programming
 #       Comments [7]
Friday, June 13, 2008 4:26:23 AM (Pacific Standard Time, UTC-08:00)
At first I didn't get your explanation... but I gave it a try and was surprised by the results :-)

I fired up reflector and saw the missing piece:

for (int i = 0; i < 10; i++)
{
actions.Add(delegate {Console.WriteLine(i);});
}

Reflector shows that a "delegate" is added to the actions array.
That was the missing piece.

Thanks for the info!
DM
Friday, June 13, 2008 9:54:26 AM (Pacific Standard Time, UTC-08:00)
Thanks for the comment DM. I should have mentioned the delegate addition to the code.
Tuesday, July 08, 2008 2:24:36 AM (Pacific Standard Time, UTC-08:00)
There's a workaround for this that works in most situations [click here], except in conjunction with yield return.
Thursday, July 17, 2008 11:26:44 PM (Pacific Standard Time, UTC-08:00)
I hope no one is not going to use lambdas :)
For me it is quite hard to understand what those codes does.
uhoh
Wednesday, July 23, 2008 8:43:38 PM (Pacific Standard Time, UTC-08:00)
Fantastic to point this out. Another reason to use ReSharper as it will warn you of closure gods getting angry...

I think some generic aspects are at least as much fun in their nastiness if ill preared like the static per closed type...

Recently I was asked to work with a large team on domain-driven work and moving to 3.5 and was informed they has banned generics due to its 'lack of stability'.

I was utterly amazing (but laughing inside).. This was after an hour of looking at IEnumerable<T> examples..

A systemic problem in the lower end of our industry! I ended my work with that group for that day on the note of 'well I cannot really help I believe if you refuse to use generics so have a think on that and let me know'...

(grin)..

We recently wrote up the basics on generic type constraints and internal DSL style in using C# 3.0 with Linq and fluent approaches to API design and exposure. TO me it is just so evolutionary. The mock libraries got there first in style it appears. At least in .NET..



Damon Wilder Carr
http://blog.domaindotnet.com/
Friday, July 25, 2008 1:48:01 AM (Pacific Standard Time, UTC-08:00)
I guess the thing to remember is that the line:

() => Console.WriteLine(i)

- is a declaration, not an invocation, and that the compiler is changing the scope of "i" to accomodate it.

Anyway - since I see you're something of a Generics champion, as I am in C++ but not C#, can I ask why it is that in C# if "mongoose" inherits from "animal", list<mongoose> DOESN'T inherit from list<animal> ?

Richard
richard develyn
Monday, September 22, 2008 9:55:40 AM (Pacific Standard Time, UTC-08:00)
There seems to be some resignation of "by design" in this discussion.
How is this nothing short of a glaring bug? A closure is a closure - and this isn't one.
Why isn't the creation of the closure-implementing class moved inside the loop so that one gets created for each closed value?
Thelazydogsback
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Live Comment Preview

Contact

Email Me Send mail to the author(s)

Calendar

<October 2008>
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

About this site

Jason Olson's thoughts on Programming, Games, Music and Life in General

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2008
Jason Olson

Sign In
All Content © 2008, Jason Olson
Theme based on 'Business' created by Christoph De Baene (delarou)