Monday, March 19, 2012

C# under the covers

More of what the compiler does without you realising it, only this time in a C# sample.

public class Class1
{
public void Test()
{
var t = Assembly.GetExecutingAssembly().GetTypes().First(x => x.Name == "Class1");
Console.WriteLine(t.FullName);
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

The absolutely simplest sort of method, which you can clearly completely cover by calling it once. Right?

That's what I thought until I tried running OpenCover over code rather like this, and it told me I had only covered one of two possible branches.

So naturally, I go "WTF?", and wonder whether it's the return or throw alternatives out of .First that it's alluding to and to be certain, crack open ILSpy, and see that first line expands to

IL_0000: nop
IL_0001: call class [mscorlib]System.Reflection.Assembly [mscorlib]System.Reflection.Assembly::GetExecutingAssembly()
IL_0006: callvirt instance class [mscorlib]System.Type[] [mscorlib]System.Reflection.Assembly::GetTypes()
IL_000b: ldsfld class [System.Core]System.Func`2<class [mscorlib]System.Type, bool> ClassLibrary2.Class1::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0010: brtrue.s IL_0025
IL_0012: ldnull
IL_0013: ldftn bool ClassLibrary2.Class1::'<test>b__0'(class [mscorlib]System.Type)
IL_0019: newobj instance void class [System.Core]System.Func`2<class [mscorlib]System.Type, bool>::.ctor(object, native int)
IL_001e: stsfld class [System.Core]System.Func`2<class [mscorlib]System.Type, bool> ClassLibrary2.Class1::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0023: br.s IL_0025
IL_0025: ldsfld class [System.Core]System.Func`2<class [mscorlib]System.Type, bool> ClassLibrary2.Class1::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_002a: call !!0 [System.Core]System.Linq.Enumerable::First<class [mscorlib]System.Type>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [System.Core]System.Func`2<!!0, bool>)
view raw gistfile1.cs hosted with ❤ by GitHub

which actually involves first caching the delegate corresponding to the lambda, if it hasn't been already; or using it on subsequent calls.

The release build is much the same; it lacks the initial nop, and the meaningless jump-to-next-instruction at offset 0x23.

Move the constant string out into the method, and make the lambda close over it

public string Test()
{
var key = "Class1";
var t = Assembly.GetExecutingAssembly().GetTypes().First(x => x.Name == key);
return t.FullName;
}
view raw gistfile1.cs hosted with ❤ by GitHub

and the caching goes away : a new instance of ClassLibrary2.Class1/'<>c__DisplayClass1' gets created every time, debug or release. And with it goes the branch.

No comments :