Monday, May 12, 2008

Anonymous Methods in C# Cause Subtle Programming Errors.

Lambda expressions and anonymous methods in C# are more complicated than you probably think. Microsoft points out that an incomplete understanding of them can result in "subtle programming errors". After running into exactly that, I'd agree. While I haven't tried it, Lambda expressions in C# 3 are supposed to do exactly the same thing.

Here's some code that didn't do what I'd originally thought it would do:

class Program {
  delegate void TestDelegate();

  static void Test( int v ) {
    System.Console.WriteLine(v.ToString());
  }

  static TestDelegate CreateDelegate() {
    int test = 0;
    TestDelegate a = delegate(){ Test(test); };
    test = 2;
    return a;
  }

  static void Main() {
    CreateDelegate()();
  }
}

This prints 2. This is not because of boxing. In fact, exactly the same thing happens if you replace the int with a reference type.

Instead, the compiler creates an anonymous inner class and moves all of the captured variables into that. All references end up pointing to the inner class, so the second assignment to the test variable actually modifies a member of this class.

Here's roughly what it looks like:

class Program
{
  delegate void TestDelegate();

  static void Test( int v ) {
    System.Console.WriteLine(v.ToString());
  }

  class __AnonymousClass {
    int test;
    void __AnonymousMethod() { Test(this); }
  }

  static TestDelegate CreateDelegate() {
    __AnonymousClass __local = new __AnonymousClass();
    __local.test = 0;
    TestDelegate a = __local.__AnonymousMethod;
    __local.test = 2;

    return a;
  }

  static void Main() {
    CreateDelegate()();
  }
}

Anything starting with a few underscores is a compiler generated name. The names I used are not correct.

Here's the catch. The local variable no longer exists. The variable you thought was local is now located inside an object created to hold your anonymous method.

Interestingly, while this is the whole story as documented by Microsoft, there is more to it. For example, it's possible to have two anonymous methods that reference the same local variable. It looks as though that variable is shared between the two anonymous method objects, but someone who's willing to disassemble the compiled code should confirm that.

You really do have to know about some of this behavior. The problem would disappear if anonymous methods could only read local variables. Then a copy of the value could be stored.

This post was originally published on the PC-Doctor blog.

No comments: