Welcome to MSDN Blogs Sign in | Join | Help

Anonymous Methods, Part 2 of ?

A lot of people seem to have trouble grasping how anonymous methods affect the lifetimes of locals.  SO hopefuly I can clarify that a little.  Previous to anonymous methods, the lifetime and the visibility of a local were identical for all practical purposes.  Because of the visibility restrictions, there was no way to tell if a loop was sharing the same instance of a local or using a new instance of a local.  Example:


void SomeMethod()
{
    int outer = 0;
    for (int i = 0; i < 10; i++)
    {
    
    int inner = i;
        Console.WriteLine("outer = {0}", outer++);
        Console.WriteLine("i = {0}", i);
        Console.WriteLine("inner = {0}", ++inner);
    }
}

Here we have 3 locals: outer, i, and inner.  Conceptually there is one instance of outer, and i for each invocation of SomeMethod. I think everybody would agree to that so far.  Now comes the tricky part: there is conceptually a unique instance of inner for each iteration of the loop or each entry into the containing scope.  That means that in this example there are 10 different instances of inner.  However, because none of the lifetimes overlap, there is no way to detect this, and the C# compiler uses the same slot for all instances.  The runtime might even use the same register for each instance.  As soon as you add anonymous methods to the mix, then lifetimes do have the ability to overlap, and suddenly your code can percieve the different instances.  As an example, we'll modify the above code to not write out the variables directly, but instead create 10 anonymous methods (one for each loop) that does the same thing.  We'll run each anonymous method twice: once inside the loop when it is created, and once after the loop has finished.


delegate void NoArgs();

void SomeMethod()
{
    NoArgs [] methods = new NoArgs[10];

    int outer = 0;
    for (int i = 0; i < 10; i++)
    {
    
    int inner = i;
        methods[i] = delegate {
            Console.WriteLine("outer = {0}", outer++);
            Console.WriteLine("i = {0}", i);
            Console.WriteLine("inner = {0}", ++inner);
        };
        methods[i]();
    }
    for (int j = 0; j < methods.Length; j++)
        methods[j]();
}


 

Now test yourself and see if you can predict the actual output.  Just remember that the number of instances hasn't changed, only their lifetimes. The first time we run the anonymous methods, we get the exact same output as before: outer counts from 0 to 9, i counts from 0 to 9, and inner counts from 1 to 10.  Now the second time we run the anonymous methods things might be a little supprising. outer continues counting from 10 to 19, i is stuck 10, and inner looks like it is counting from 2 to 11!  If you think you know why, post a comment, or email me.  If this is so simple that I should stop wasting my time and yours, let me know.  Otherwise, I explain the whys and hows in my next few posts.

Here's the acutal output:


outer = 0
i = 0
inner = 1
outer = 1
i = 1
inner = 2
outer = 2
i = 2
inner = 3
outer = 3
i = 3
inner = 4
outer = 4
i = 4
inner = 5
outer = 5
i = 5
inner = 6
outer = 6
i = 6
inner = 7
outer = 7
i = 7
inner = 8
outer = 8
i = 8
inner = 9
outer = 9
i = 9
inner = 10
outer = 10
i = 10
inner = 2
outer = 11
i = 10
inner = 3
outer = 12
i = 10
inner = 4
outer = 13
i = 10
inner = 5
outer = 14
i = 10
inner = 6
outer = 15
i = 10
inner = 7
outer = 16
i = 10
inner = 8
outer = 17
i = 10
inner = 9
outer = 18
i = 10
inner = 10
outer = 19
i = 10
inner = 11

--Grant

Published Monday, March 08, 2004 6:07 PM by grantri
Filed under:

Comments

# re: Anonymous Methods, Part 2 of ?

Can it be because the anonymous method has its own copy of inner, created and initialized when it's created to value sof i, 0 to 9, then incremented the first time we call it (just after we create the anonymous method) so after the first loop the values are 1 to 10 and those are incremented once more during the second loop, rendering values of 2 to 11.

Personally I think anonymous methods are not a good idea, since it will lead to code that is very difficult to read and understand.
Monday, March 08, 2004 6:39 PM by Jerry Pisk

# re: Anonymous Methods, Part 2 of ?

The anonymous method does not have its own copy of the inner variable. Instead, it shares it with the calling function.

Here's the catch. The inner variable is not stored in the stack, but is created anew each iteration of the loop. A new object is created with an member representing "inner." Both the function and the anonymous methods bother refer to this new object, call it anonymous, and accesses anonymous.inner.
Monday, March 08, 2004 8:52 PM by Wesner Moise

# re: Anonymous Methods, Part 2 of ?

Wesner, that's what I was trying to say - each loop creates new instance of the inner variable which is then tied to that loop's anonymous function. I said it was the anonymous function's copy because once the loop body goes out of scope each iteration it is the only code that can reference it, each anonymous function having its own copy.
Monday, March 08, 2004 9:48 PM by Jerry Pisk

# re: Anonymous Methods, Part 2 of ?

Okay guys. The anonymous method causes the entire body of SomeMethod to be captured in a 'closure'. That means all local variables become part of a closure class that is referenced by each anonymous method. Basically, they all share the same 'tear-off' stack, where the locals have been allocated. The reason the first pass seemed to work is because each delegate was called inline with the loop. When called out of line, like the latter 'j' loop, since only the inner body of the delegates are executed, the inner and outer numbers keep incrementing. The loop variable 'i' stays constant because that's where the loop left it. The fact that the 'inner' variable seems to start over at 1 or 2 is odd, but its scope was exited so I suspect the compiler is free to do with it what it will.

Either that or its magic.
Monday, March 08, 2004 10:49 PM by Matt

# re: Anonymous Methods, Part 2 of ?

I don't think the fact that inner starts at 2 in the second loop is odd. Think about, the first time the first loop runs it creates a first copy of inner. It initializes it to the value of i, which is 0 at the moment. Then the first copy of the anonymous function is called, incrementing its inner to 1 and printing out 1. And when the same first copy of your anonymous function gets called in the second loop it increases its copy of inner once more, to 2 and prints that out. Simple as that.

The point is that you end up with ten inner variable copies and ten anonymous functions after the first loop. Each anonymous function has its own state (or stack, call it whatever) with its own inner variable. The rest, i and outer, is trivial.
Tuesday, March 09, 2004 12:36 AM by Jerry Pisk

# re: Anonymous Methods, Part 2 of ?

Everything makes sense to me, apart from variable i. It seems it would be more intuitive for the C# compiler to capture i just like inner and outer are captured.

When I read the C# v2 spec it never occured to me that for loop variables wouldn't be captured. Are there any other places where variables are not captured?

Implied from this blog post, I would therefore expect that these two code snippes would produce different IL, and hence different results:

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

and

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

even though most people would consider them identical.


Tuesday, March 09, 2004 4:47 AM by RichB

# Thoughts on Anonymous Methods and the other .Net v2 Enhancements

Monday, March 29, 2004 1:17 PM by Aimless .Net Development

# Templates, Partial classes, Iterators and anonymous methods.

The big framework features.
Thursday, April 14, 2005 3:18 PM by Smartwombat Blog

# Anonymous methods and delegates

I&#39;m studying C# 2.0 and was looking into anonymous methods this morning. (One of Don Box&#39;s 5

Saturday, November 17, 2007 3:38 PM by jim blizzard's blog
Anonymous comments are disabled
 
Page view tracker