Partager via


Fun with Anonymous Methods

A recent thread over an internal alias really drilled home some details of how Anonymous Methods in C# 2.0 work…

If you have the Whidbey Beta you can check it out for yourself, but try to figure it out without running the code…

What does this code print:

using System;

using System.Threading;

class Program

{

    static void Main(string[] args)

    {

        for (int i = 0; i < 10; i++)

        {

            ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(i); }, null);

        }

        Console.ReadLine();

    }

}

How does this change if you tweak it like this:

using System;

using System.Threading;

class Program

{

    static void Main(string[] args)

    {

        for (int i = 0; i < 10; i++)

        {

            int j = i;

            ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(j); }, null);

        }

        Console.ReadLine();

    }

}

And most importantly, why?

Comments

  • Anonymous
    August 03, 2004
    Easy: The code above will print a whatever current value of i when thread's Console.WriteLine is called, and in the second case numbers from 1 to 10 (may be not strictly in that order) will be printed.

  • Anonymous
    August 03, 2004
    No :]

  • Anonymous
    August 03, 2004
    I would think that in both cases, the compiler generates pseudo class with i and j as public member variables of the class (since both variables are scoped to the function main()). It then instantiates this class and assigns the value of i and j (in each scenario) respectively to the member variable. So, I think it would print 1 to 10 (not necessarily in that order) in both cases.

  • Anonymous
    August 03, 2004
    I'am sorry. 0 to 9. Not 1 to 10. classic loop mistake :)

  • Anonymous
    August 03, 2004
    What Andrei said.

    As for why, i is created in Main()'s scope (i'm pretty sure loop variables like i are created in the containing scope, anyway... have to dig up a copy of the standard and check sometime), and subsequently rebound on every iteration through the for loop. Since the closure created by the anonymous delegate just captures a reference to i, any changes to it can be observed from within the closure.

    In the second example, j is created within the for loop, and hence, each closure captures a unique instance of it.

    At least, i think that's what's happening...

  • Anonymous
    August 03, 2004
    It actually makes sense but it took me some staring at the generated code.

    1. The compiler generates a class (lets call it D) that has a method that contains the code for the anonymous method.
    2. The captured variables of anoymous method are member variables of the class D. Note that only referenced variables are captured. (i in the first, j in the second case)

    Now comes the important observation:

    The lifetime of j is one iteration while the lifetime of i is the entire loop!

    So class D is intantiated differently in the 2 cases:
    In the first case it is intantiated once for the whole looop while in the second case it is intantiate in each iteration.

    It all makes sense. If you move j outside of the loop then you get identical results. (Well actually the results depend on the the scheduler but that's a different story)

  • Anonymous
    August 03, 2004
    Whoa. The 1st test wasn't at all what I expected, but it makes sense upon reflection. The 2nd test made sense once I understood the 1st test.

  • Anonymous
    August 03, 2004
    Is this supposed to be an example of how confusing C# is?

  • Anonymous
    August 03, 2004
    Is this supposed to be an example of how confusing C# is?

  • Anonymous
    August 03, 2004
    I agree with Chris - suprised until the sudden "aha!"

  • Anonymous
    August 03, 2004
    Hmm, Correct me if I am wrong, I thought that Class D (in Cris) email will be create 10 instance regardless of the each test.

    In fact, I thought that the member variable are all reference the i and j respectly.
    The issue is that int is a value type, thus the act of
    int j = i;
    inside the loop will force a new instance of int to be created. Susequencely the anonymous method(class D) will take a reference of the newly create int instance. GC recongize that there are still reference to that new int -- so at the end of the each iteration, j doesn't get GCed. Next interation, another new j integer will be created and another instance of anomynous method(class D) will take a reference on it....

    That's the reason the second method will generate 0-9 (not necessary in the order but complete)

    But the first method, it will print out whatever value that when the thread run the value i is (since there is only one copy of i in the GC, but there are 10 different j inside the GC)

    -- Am I wrong??

  • Anonymous
    August 03, 2004
    The comment has been removed

  • Anonymous
    August 05, 2004
    Many of you got this one... here is what the C# compiler team says about it:
    Anonymous methods do not capture read-only locals at a point in time, instead they actually capture the local by putting it on the heap, to share it between the outer method and all anonymous methods. The threadpool could kick-off at any moment within the loop and thus print out just about any number between 1 and 10, but on a single proc machine, the thread pool probably won't start until the Sleep occurs on the main thread, and thus all the delegates will just print the current value of "i" which is 10.

    Anonymous methods are not closures or completions. They are anonymous methods. They allow sharing of variables with the outer method (not copy in, or copy out, or copy in-out, or even by ref).

    The example you give below will print 100 - 110, assuming that all of the thread pool threads run synchronously, otherwise the "x++" could get a little messed up and end up with different results.

    If you want a new local for each iteration of the loop, you need to declare it inside the for loop, like this:
    for (int i = 0; i < 10; i++)
    {
    int j = i;
    ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(j); j++;}, null);
    }
    Thread.Sleep(100);

    Then you'll get what you seem to want. In this case "j" is inside the loop, and thus each iteration or (entry of the scope) gets a new instance of "j" and that instance is specific to that iteration. Because of that, the compiler is forced to create 10 delegates instead of 1 and each delegate uses it's instance of "j". If you want to try something else, try this code:
    for (int i = 0; i < 10; i++)
    {
    int j = i;
    ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(j); j++;}, null);
    ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(j); j++;}, null);
    }
    Thread.Sleep(100);

  • Anonymous
    August 05, 2004
    Note that Brad's quoted explanation above discusses a variation of the puzzle different from the one he posted in this article, so bear that in mind when you see things that don't match the puzzle ("the Sleep occurs on the main thread" - there is no Sleep call in our puzzle; "the example you give below will print 100-110" - Brad didn't include that example in the puzzle)...

  • Anonymous
    August 16, 2004
    "anonymous methods are not closures"

    I've heard this a couple of times now - I think Eric Gunnerson mentioned it on a Channel 9 video too.

    The thing is, I'm aware of at least two definitions of what constitutes a lexical closure. If you take the one on the c2 wiki (a function with an associated map of variable names onto values) then no, a C# anonymous method doesn't look at all like that kind of closure because changes made by the child scope propagate back out to the parent scope.

    But the other definition I'm aware of is subtly different: it says that a closure is a function, bundled with its environment at the point at which the closure was created.

    Now for some languages there's actually no difference between these two definitions. Specifically, if your language doesn't support mutable variables, then there's no detectable difference between the two - it doesn't really matter if the closure gives you access to the same variable as the outer scope, or just remembers its value - if the value never changes then that's just an implementation detail. And I've always preferred the idea of a bundled environment because the other definition - a name/value lookup seems so implementation-focused...

    But for languages with mutable variables (e.g. C#), then these two definitions are very different. If you think that closures involve remembering the values of the relevant variables when the closure was created, then this is clearly not what C# is doing. But if you consider them to simply bundle your lexical environment, then what C# does is exactly what you would expect: if I refer to some variable by name outside the anonymous function and inside it, then these bind to exactly the same entity because the same environment is available to me in the outer and inner functions. Since it's the same environment, it's the same variable.

    There's a common LISP example that crops up in various places on the internet showing two distinct lexical closures bound to the same variable, with the updates made by one visible to the other.

    So it has perplexed me when people keep saying C# anonymous functions aren't closures. They aren't like one of the definitions - the rather implementation-focused definition. But they are, as far as I can tell, exactly like the other definition. Have I missed something?

  • Anonymous
    August 08, 2005
    Anonymous Method
    One of the new features in C#2.0 being shipped in with Whidbey is anonymous methods....

  • Anonymous
    August 08, 2005
    Anonymous Method
    One of the new features in C#2.0 being shipped in with Whidbey is anonymous methods....

  • Anonymous
    March 19, 2008
    PingBack from http://cityjokesblog.info/brad-abrams-fun-with-anonymous-methods/

  • Anonymous
    March 31, 2008
    PingBack from http://collegefunfactsblog.info/brad-abrams-fun-with-anonymous-methods/

  • Anonymous
    May 08, 2008
    PingBack from http://blog.jozilla.net/2008/05/09/back-to-the-future-smalltalk/

  • Anonymous
    May 29, 2009
    PingBack from http://paidsurveyshub.info/story.php?title=brad-abrams-fun-with-anonymous-methods

  • Anonymous
    June 12, 2009
    PingBack from http://insomniacuresite.info/story.php?id=1989