Thursday, June 05, 2008

C# Anonymous Delegate Pitfall

What is output of following code?
using System;
using System.Collections.Generic;
using System.Text;

namespace AnonymousDelegate
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("AnonymousDelegate1:");
AnonymousDelegate1();
Console.WriteLine();

Console.WriteLine("AnonymousDelegate2:");
AnonymousDelegate2();
Console.WriteLine();

Console.WriteLine("AnonymousDelegate3:");
AnonymousDelegate3();
Console.Read();
}

static void AnonymousDelegate1()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(delegate() { Console.WriteLine(i); });
}
foreach (Action action in actions)
{
action();
}
}

static void AnonymousDelegate2()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int temp = i;
actions.Add(delegate() { Console.WriteLine(temp); });
}
foreach (Action action in actions)
{
action();
}
}

static void AnonymousDelegate3()
{
List<Action> actions = new List<Action>();
int temp;
for (int i = 0; i < 3; i++)
{
temp = i;
actions.Add(delegate() { Console.WriteLine(temp); });
}
foreach (Action action in actions)
{
action();
}
}
}
}
The result may surprise you:
AnonymousDelegate1:
3
3
3

AnonymousDelegate2:
0
1
2

AnonymousDelegate3:
2
2
2
Why is inconsistent for each test? That stems from the way how .NET handle anonymous delegate or anonymous method.

.NET compiler will create a delegate and a static method for an anonymous delegate, just like a traditional delegate. There's no anonymous concept in intermediate language (IL) in any .NET assembly.

The thing becomes more interesting when a variable used inside anonymous delegate/method is declared outside the anonymous delegate/method, which is called closure scenario. .NET compiler will wrap the anonymous method and the variable to a sealed class: anonymous method becomes a class method, variable becomes a public member. We can examine this by looking inside the IL code (click to see bigger picture):



As we can see there're 3 inner classes (<>c__DisplayClass2/5/9 in red boxes) generated for 3 anonymous methods. All three classes have the same structure:
private sealed class ComiplerGeneratedClass
{
public int i;
public void ActionMethod()
{
Console.WriteLine(this.i);
}
}
That makes sense but why having different results? Let's check the IL code for AnonymousDelegate1:



We can see that only one AnonymousClass object is created, and AnonymousClass.i is used for looping condition (for loop) check.

But when we look at the IL code for method of AnonymousDelegate2, we found AnonymousClass is created 3 times inside the for loop. To be more readable, we translate the IL code back to C# code for all three test methods:
      private sealed class AnonymousClass // Generated by compiler
{
public int i;
public void ActionMethod()
{
Console.WriteLine(this.i);
}
}

static void AnonymousDelegate1()
{
AnonymousClass anonymousClass = new AnonymousClass();
List<Action> actions = new List<Action>();
anonymousClass.i = 0;
for (; anonymousClass.i < 3; anonymousClass.i++)
{
Action action = new Action(anonymousClass.ActionMethod);
actions.Add(action);
}
foreach (Action action in actions)
{
action.Invoke();
}
}

static void AnonymousDelegate2()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
AnonymousClass anonymousClass = new AnonymousClass();
anonymousClass.i = i;
Action action = new Action(anonymousClass.ActionMethod);
actions.Add(action);
}
foreach (Action action in actions)
{
action.Invoke();
}
}

static void AnonymousDelegate3()
{
AnonymousClass anonymousClass = new AnonymousClass();
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
anonymousClass.i = i;
Action action = new Action(anonymousClass.ActionMethod);
actions.Add(action);
}
foreach (Action action in actions)
{
action.Invoke();
}
}
Now the answer is clear. .NET compiler generates different code depending on the scale of closure variable. It looks very confusing at the beginning and it's easy to write buggy code if we don't understand this.

We have to put it in mind that a real class is created, and a public member is used to store a closure variable for anonymous methods with closure. If not sure how the closure variable is handled, use minimum scale of local variable and pass it to the anonymous method, like what AnonymousDelegate2 does; that ensures each anonymous method using independent variable, but be aware of consuming more resource in such case because more anonymous classes are generated at run time.