Microsoft .Net and Visual Studio (especially the latest 2010 over 4.0) offer many automatic tools and code sugar that allows us to write code faster and safer.
But, every once in a while it is recommended to peek under the hood and see what the compiler had in mind, and how far it is from what we originally meant in our code.
I do that using the .Net Reflector, lately becoming a paid product and yet worth its price, at least for me.
For example- I guess you're already familiar with Lambda expressions. But, there are situations where using Lambda expressions without caution might create severe bugs and performance problems.
See the following code which you can copy to your Visual Studio and test:
public static int CalculateFactorial(int number)
{
int result = 1;
for (int i = 2; i <= number; i++)
{
result *= i;
}
return result;
}
public static void Test()
{
for (int counter = 1; counter <= 10; counter++)
{
ThreadPool.QueueUserWorkItem(state =>
Console.WriteLine("Factorial of {0} is {1}",
counter, CalculateFactorial(counter)));
}
}
It looks like we've created an efficient and elegant piece of code, aimed to calculate factorials of numbers between 1 and 10 while making the most of our computer processors, as we're breaking these calculations to different threads.
So, where's the catch?
Let's open the .Net reflector and see the compiled code for the "Test" method:
public static void Test()
{
for (int counter = 2; counter <= 10; counter++)
{
ThreadPool.QueueUserWorkItem(delegate(object state)
{
Console.WriteLine("Factorial of {0} is {1}", counter, CalculateFactorial(counter));
});
}
}
OK, so we found out that Lambda expressions are actually compiled into .Net
anonymous delegates. Lambda expressions are only code sugar..
Big deal. Still, where's the catch??
The first catch is that .Net reflector is smart enough not to display the actual
code, but more of a sweetened version of the code.
In order to see the actual code we either need to view it in IL, or change
the reflector optimization mode to .Net 1.0.
Here's the code once again, in .Net 1.0 mode:
public static void Test()
{
WaitCallback CS$<>9__CachedAnonymousMethodDelegate1=null;
<>c__DisplayClass2 CS$<>8__locals3 = new <>c__DisplayClass2();
CS$<>8__locals3.counter = 2;
while (CS$<>8__locals3.counter <= 10)
{
if (CS$<>9__CachedAnonymousMethodDelegate1 == null)
{
CS$<>9__CachedAnonymousMethodDelegate1 =
new WaitCallback(CS$<>8__locals3.<Test>b__0);
}
ThreadPool.QueueUserWorkItem(
CS$<>9__CachedAnonymousMethodDelegate1);
CS$<>8__locals3.counter++;
}
}
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
// Fields
public int counter;
// Methods
public <>c__DisplayClass2()
{
}
public void <Test>b__0(object state)
{
Console.WriteLine("Factorial of {0} is {1}",
this.counter,
CalculateFactorial(this.counter));
}
}
Browsing through the above generated code, we now find out that anonymous delegates
are also virtual code elements that compile into something else, or- syntactic sugar.
The compiler actually creates a nested class with some kind of garbled name
(in order to prevent code collisions with our code).
This class holds a method that has the implementation of our original "anonymous
delegate", to which the compiler directs the ThreadPool.QueueUserWorkItem delegate
parameter.
We were writing .Net v1.0/2.0 code all along and didn't even know it!...
OK. Now that we understand that, let's see what happened to our "counter" variable-
the compiler added a public field to the nested class holding the implementation
method, by the name of "counter". This counter field is being incremented once for
each loop cycle, just after the internal <Test>b__0 method is assigned for another
ThreadPool thread!
This means that the counter, being a shared resource, will probably make it all the
way up to 10 before the first thread even starts to run, thus causing all of these
threads to calculate the factorial of 10, instead what we wanted them to do:
calculate the factorials of all numbers from 1 to 10…
Such a problem is called: "Captured variables" and can be avoided by passing the
counter as "state" parameter into the delegate, thus causing the delegate to own
a "private" copy of the counter for each thread:
public static void Test()
{
for (int counter = 1; counter <= 10; counter++)
{
ThreadPool.QueueUserWorkItem(state =>
Console.WriteLine("Factorial of {0} is {1}",
(int)state,
CalculateFactorial((int)state)), counter);
}
}
You can copy the above code to your Visual Studio and see that it now prints the correct
results. You can also open it with .Net Reflector to see what was built now.
You might be surprised…