Sunday, September 10, 2006 1:15 AM bart

C# anonymous methods in depth

Introduction

C# 2.0 is out there for quite a while now but I still have some in-depth posts on my to-blog-list. This time, I'm going to focus on the inner workings of anonymous methods.

Delegates

Delegates are managed function pointers. Classic example:

using System;
using
System.Threading;

class Demo
{
   public static void
Main()
   {
      ThreadStart ts =
new
ThreadStart(Do);
      Thread thread =
new
Thread(ts);
      thread.Start();
      thread.Join();
   }

   static void
Do()
   {
      Console.WriteLine("Something"
);
   }
}

In here, ThreadStart is a delegate. It takes a method as a parameter (the function pointer) and allows that method to be invoked through the delegate. No reason to discuss this well-known principle in much more depth over here right now.

C# 2.0 - Making things easier

The code above can be shortened in various ways in C# 2.0:

using System;
using
System.Threading;

class Demo
{
   public static void
Main()
   {
      Thread thread = new Thread(Do);
      thread.Start();
      thread.Join();

   }

   static void
Do()
   {
      Console.WriteLine("Something"
);
   }
}

No ThreadStart in here anymore, the compiler takes care of that (because it knows the overloads of the Thread class constructor and can look for a delegate parameter that has a compatible signature with the Do method). Nevertheless, the compiler will emit ThreadStart stuff:

IL_0007: ldnull
IL_0008: ldftn void T::Do()
IL_000e: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, native int)

Don't worry about the object parameter for now; the native int parameter is where the function pointer (retrieved by ldftn) is stored.

We can take simplification of the code still a little further:

using System;
using
System.Threading;

class Demo
{
   public static void
Main()
   {
     
Thread thread = new Thread(delegate () { Console.WriteLine("Hello"); });
      thread.Start();
      thread.Join();

   }
}

Now we're using an anonymous method. Notice we didn't mention ThreadStart again, just the core implementation of what the thread should do:

delegate () { Console.WriteLine("Hello"); }

Note: We need to specify the empty parameter list (()) in here because there's another delegate that could match our anonymous method otherwise:

t.cs(10,25): error CS0121: The call is ambiguous between the following methods
        or properties:
        'System.Threading.Thread.Thread(System.Threading.ThreadStart)' and
        'System.Threading.Thread.Thread(System.Threading.ParameterizedThreadStart)'

How does this work? Start by the Main's IL code (omitted nop's for clarity):

.method public hidebysig static void Main() cil managed
{
   .locals init (class [mscorlib]System.Threading.Thread V_0)
   IL_0001: ldsfld class [mscorlib]System.Threading.ThreadStart Demo::'<>9__CachedAnonymousMethodDelegate1'
   IL_0006: brtrue.s IL_001b
   IL_0008: ldnull
   IL_0009: ldftn void Demo::'<Main>b__0'()
   IL_000f: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, native int)
   IL_0014: stsfld class [mscorlib]System.Threading.ThreadStart Demo::'<>9__CachedAnonymousMethodDelegate1'
   IL_0019: br.s IL_001b
   IL_001b: ldsfld class [mscorlib]System.Threading.ThreadStart Demo::'<>9__CachedAnonymousMethodDelegate1'
   IL_0020: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
   IL_0025: stloc.0
   IL_0026: ldloc.0
   IL_0027: callvirt instance void [mscorlib]System.Threading.Thread::Start()
   IL_002d: ldloc.0
   IL_002e: callvirt instance void [mscorlib]System.Threading.Thread::Join()
   IL_0034: ret
} // end of method Demo::Main

There are two key things in here to notice. First, the's a so-called "cached anonymous method delegate" that's kept by our class. Nothing special, just a field of - in this case - type System.Threading.ThreadStart:

.field private static class [mscorlib]System.Threading.ThreadStart '<>9__CachedAnonymousMethodDelegate1'

IL instructions IL_0008 to IL_0019 initialize this field if it hasn't been initialized (tested in IL_0006) yet.

The second important thing to notice is the <Main>b__0 thing in line IL_0009. This one is a method that contains the anonymous method's implementation:

.method private hidebysig static void '<Main>b__0'() cil managed
{
   .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
   IL_0001: ldstr "Hello"
   IL_0006: call void [mscorlib]System.Console::WriteLine(string)
   IL_000c: ret
} // end of method Demo::'<Main>b__0'

So, just some compiler magic that creates a private (compiler-generated) method with the anonymous method's implementation. But wait, there's more to come.

Anonymous methods to the max

What about the following piece of code?

using System;
using
System.Threading;

class
Demo
{
   public static void
Main()
   {
      string msg = "Hello"
;

      Thread thread = new Thread(delegate () { Console
.WriteLine(msg); });
      thread.Start();
      thread.Join();
   }
}

The tricky part in here is the msg variable that's being used by the anonymous method but that has been defined outside that anonymous method. Indeed, the compiler just gets away with this one too! Let's see how:

.method public hidebysig static void Main() cil managed
{
   .locals init (class [mscorlib]System.Threading.Thread V_0,
class Demo/'<>c__DisplayClass1' V_1)
   IL_0000: newobj instance void Demo/'<>c__DisplayClass1'::.ctor()
   IL_0005: stloc.1
   IL_0007: ldloc.1
   IL_0008: ldstr "Hello"
   IL_000d: stfld string Demo/'<>c__DisplayClass1'::msg
   IL_0012: ldloc.1
   IL_0013: ldftn instance void Demo/'<>c__DisplayClass1'::'<Main>b__0'()
   IL_0019: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, native int)
   IL_001e: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
   IL_0023: stloc.0
   IL_0024: ldloc.0
   IL_0025: callvirt instance void [mscorlib]System.Threading.Thread::Start()
   IL_002b: ldloc.0
   IL_002c: callvirt instance void [mscorlib]System.Threading.Thread::Join()
   IL_0033: ret
} // end of method Demo::Main

Again, two things are subject to further investigation. The first is the mysterious <>c__DisplayClass1 which is defined as follows:

.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass1'
extends [mscorlib]System.Object
{
   .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
} // end of class '<>c__DisplayClass1'

.field public string msg

So, essentially, this class is a wrapper around a string field which contains our parameter. If we'd have used more variables in the anonymous method definition that are defined outside the anoymous method's scope, we'd end up with a class that encapsulates these other "implicit parameters" too.

On to the <Main>b__0 method that's defined in this nested "display class" too:

.method public hidebysig instance void '<Main>b__0'() cil managed
{
   IL_0001: ldarg.0
   IL_0002: ldfld string Demo/'<>c__DisplayClass1'::msg
   IL_0007: call void [mscorlib]System.Console::WriteLine(string)
   IL_000d: ret
} // end of method '<>c__DisplayClass1'::'<Main>b__0'

No magic in here, this method is just using the msg field of the nested class to print the message to the screen. Simple yet efficient trick to make anonymous methods so powerful.

Note: In this particular sample we could get around all of this magic compiler work if we used the ParameterizedThrreadStart delegate which allows you to pass an object to the thread upon start.

Usage guidelines for anonymous methods

Anonymous methods are great but don't overuse it. If the body of the anonymous method contains say more than 3-5 lines of code, consider to create a separate method. Similarly, if you are passing a lot of "implicit parameters" in to the anonymous method, it might be easier to understand if you just create a helper class wrapping the method to be called through a delegate but with access to (copies of) the required parameters.

With the lambda expression feature in C# 3.0 coming up, some basic understanding of anonymous methods is no waste of time. I hope this post helped.

Ciao!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: , ,

Comments

No Comments