On anonymous delegates

Recently, doing some searches on anonymous methods/delagates, I came into an interesting puzzle posted by Brad Adams
The puzzle (and its explaination) are very interesting to understand what happens under the hood when an anonymous delegate is created. However, it took me some effort to understand what the compiler did using only the post and its comments, so I put Brad's code into Visual Studio, compiled an assembly, and inspected it with Reflector. The first case, the one with the delegator binding the loop variable i (num1, in the reverse engineered code) is translated in the following way:



private static void Main(string[] args)
{
      for (int num1 = 0; num1 < 10; num1++)
      {
            Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
            class1.j = num1;
            ThreadPool.QueueUserWorkItem(new WaitCallback(class1.
b__0), null);
      }
      Console.ReadLine();
}

the anonymous delegate shares i between caller and callee. It is a closure, in a wide sense, in that it captures the state when it is created. However, since the C# compiler capture i "by reference" and in this case the closure is created before the for loop, the behaviour is different from the one expected in real closures (in fact, anonymous delagates are not closures)

The second example however (delegate bound to a variable local to the for loop) is different: the lifetime of j (the for block) forces the compiler to create and initalize a state object for the delegate at each iteration inside the loop:


private static void Main(string[] args)
{
WaitCallback callback1 = null;
Program.<>c__DisplayClass2 class1 = new Program.<>c__DisplayClass2();
class1.i = 0;
while (class1.i < 10)
{
if (callback1 == null)
{
callback1 = new WaitCallback(class1.
b__0);
}
ThreadPool.QueueUserWorkItem(callback1, null);
Thread.Sleep(100);
class1.i++;
}
Console.ReadLine();
}


public void
b__0(object)
{
Console.WriteLine(this.i);
}

internal class Program
{
// Methods
public Program();
private static void Main(string[] args);

// Nested Types
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
// Methods
public <>c__DisplayClass2();
public void
b__0(object)
{
Console.WriteLine(this.i);
}

// Fields
public int i;
}
}

In a comment of Brad post, a member of the C# team pointed out that

"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."

Surely this statement makes clear what is happening: however, this example prove one points I always believed: the only way to be sure of what a compiler do is to look at the assembler code it generates.


Copyright 2020 - Lorenzo Dematte