Freigeben über


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 = 0i = 0inner = 1outer = 1i = 1inner = 2outer = 2i = 2inner = 3outer = 3i = 3inner = 4outer = 4i = 4inner = 5outer = 5i = 5inner = 6outer = 6i = 6inner = 7outer = 7i = 7inner = 8outer = 8i = 8inner = 9outer = 9i = 9inner = 10outer = 10i = 10inner = 2outer = 11i = 10inner = 3outer = 12i = 10inner = 4outer = 13i = 10inner = 5outer = 14i = 10inner = 6outer = 15i = 10inner = 7outer = 16i = 10inner = 8outer = 17i = 10inner = 9outer = 18i = 10inner = 10outer = 19i = 10inner = 11


--Grant

Comments

  • Anonymous
    March 08, 2004
    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.
  • Anonymous
    March 08, 2004
    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.
  • Anonymous
    March 08, 2004
    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.
  • Anonymous
    March 08, 2004
    The comment has been removed
  • Anonymous
    March 08, 2004
    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.
  • Anonymous
    March 08, 2004
    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.


  • Anonymous
    April 14, 2005
    The big framework features.
  • Anonymous
    November 17, 2007
    The comment has been removed