I posted an interesting bit of C# a couple weeks ago relating to anonymous delegates. I asked in that post what the result of the following code is. Here is the code and then the result
Code:
using System;
using System.Threading;
namespace AnonMethods
{
class Program
{
static void Main(string[] args)
{
for (Int32 i = 0; i < 20; i++)
{
Thread t = new Thread(delegate() { Thread.Sleep(10); Console.WriteLine(i); });
t.Start();
}
Thread.Sleep(5000);
}
}
}
Result:
6
6
6
6
6
9
12
12
12
13
13
13
15
15
19
20
20
20
20
20
Why?
That's the strangest result from a for loop that I've ever seen. First, I'd suggest that you compile the code for yourself (remember, C# Express is free) and then take and then decompile it in either ildasm or Reflector. I find the C# in Reflector a whole lot easier to read than the IL, so I'd recommend that you start with it. I imagine that you'll get a real kick out of what you see.
First, I'd like to caveat my explanation by saying that this is a CLR guy's explanation of what the C# compiler is doing. I haven't talked to the C# team, but just took a look at the code emitted by the compiler, which both you and I are free to ponder. All that being said, here's the basic idea of what I think is going on without going into great detail of the mechanics of anonymous delegates.
Once the C# source code is compiled, the anonymous delegate goes away and is replaced by the mechanism that I'm about to describe. Put another way, anonymous delegates are a C# source-code-only feature. For every anonymous delegate, there is a strangely, but probably predictably, named nested class added to the class that indirectly contained (within one of the methods within the class) the anonymous delegate. The class, as you might expect, exposes some surface area that is accessed from the parent class. The class is actually private, which means it cannot be accessed from anywhere but the parent class. There is a default constructor, which does nothing. There is a method that is the anonymous delegate transformed into a regular named method. Remember, anonymous delegates are a C#, not a CLR feature, so the CLR does need a named method to call. Lastly, there are a set of fields exposed on this compiler-generated class -- and this is the part that gets interesting -- that are the local variables that are accessed from both the containing method and the anonymous delegate. Variables that are defined in the containing method but not accessed from within the anonymous delegate or that are defined and used within the anonymous delegate do not get this special treatment. As you might guess, the C# compiler wires up the method that takes the delegate and the generated named method. In addition, all accesses to the shared local variables are wired up through the public fields on the generated class instead of through the actual locals. Very interesting.
OK, so that's the basic explananation of how anonymous delegates work. There are some other subtleties that I noticed, but they are not important for this explanation. I'm going to get further insight on those from the C# team and will post on those when I learn more about them.
So, then, how does all that lead to the strange and unexpected results from the for loop. Well, since the "i" variable is accessed by both the containing method and the anonymous delegate, the C# compiler rewires all accesses to "i" to the "i" field on the generated class. And since each call to the anonymous delegate in our example does its work on a separate thread, then "i" is updated by the for loop on a different schedule than it is written by Console.WriteLine in the anonymous delegate. That's why the value printed out by each call to the anonymous delegate is essentially a race condition between the for loop continually iterating and threads being created and getting time on the processor(s).
Another aspect of this is that shared locals with anonymous delegates essentially turn value types into reference types. That's a really strange behaviour and realization.
Like I said in the earlier post, the trick is determining a scenario where this behaviour is useful. I haven't found that yet, although I'm sure it will be very interesting once I find it.