Pages

October 26, 2010

Side Effect Of Type Constraints in .NET

Experimenting With Generic Constraints


As I was doing some experiments as exercise in reminding myself the difference between virtual and non-virtual method overloading behavior in C#, and stumbled on an unexpected side-effect of the constraint mechanism in Generics.

I have class B inherit from A, and wanted to write a generic test harness that would invoke methods f1 and f2 on an instance passed in, but with a generic capability to have the runtime cast the instance to the most specific type. So if I passed in B, the variable would be manipulated as B, etc. So I thought a generic method would deal with this quite nicely: void Do<T>(T arg) { arg.f1(); arg.f2; }. Now, the declaration will not compile as is, the compiler insists to know the type of T so that it can figure out whether the class supports f1(), f2() functions, fine, i go ahead and add where T : A to the signature, which turns out to introduce the side effect which is the subject of this blog entry.

When I implemented this scheme (see code below) I was surprised that even when invoking Do<B>(bInstance); , bInstance was treating as if it was A.

So the generic behavior is very different from what one would thing to be logical, and in fact C++ template mechanism does do what one expects if you were to implement a similar method, bInstance will be treated as a B.

using System;
using System.Text;

namespace SimpleMethodInheritanceTest
{
    class A
    {
        internal virtual void f1() { System.Console.WriteLine("f1 in A"); }
        internal void f2() { System.Console.WriteLine("f2 in A"); }
    }

    class B : A
    {
        internal override void f1() { System.Console.WriteLine("f1 in B"); }
        internal void f2() { System.Console.WriteLine("f2 in B"); }
    }

    class Program
    {
        static void DoBase(A a)
        {
            System.Console.WriteLine("======================== DoBase ========================");
            a.f1();
            a.f2();
        }

        static void DoBBB(B a)
        {
            System.Console.WriteLine("======================== DoBBB ========================");
            a.f1();
            a.f2();
        }


        static void Do<T>(T a) where T : A
        {
            System.Console.WriteLine("======================== Do<T> ========================");
            var specificClass = a.GetType().Name;
            System.Console.WriteLine("======================== Do<T>: T = {0}", specificClass);
            a.f1();
            a.f2();
        }
        static void Main(string[] args)
        {
            var a = new A();
            DoBase(a);
            Do<A>(a);
            var b = new B();
            DoBase(b);
            Do<B>(b);
            DoBBB(b);
            System.Console.ReadKey();
        }
    }
}

Output


I expected line 14 to say f2 in B, since T = B, so I was expecting the argument to Do<T>() to be treated as a B. But instead I get f2 in A!!!
======================== DoBase ========================
f1 in A
f2 in A
======================== Do<T> ========================
======================== Do<T>: T = A
f1 in A
f2 in A
======================== DoBase ========================
f1 in B
f2 in A
======================== Do<T> ========================
======================== Do<T>: T = B
f1 in B
f2 in A
======================== DoBBB ========================
f1 in B
f2 in B

Discussion



So we would expect D: T = B to have f2 in B, but we got f2 in A instead. So to investigate as to what might be going on under the covers, I decompiled the executable into IL using redgate's reflector.

Let's take a look at the results below.

Do<T> Dissassembled



If we dissasemble D<T> we see that on Line 25 f2 is explicitly invoked on class A. This is causes the unexpected results of f2 in A.

.method private hidebysig static void Do<(SimpleMethodInheritanceTest.A) T>(!!T a) cil managed
{
    .maxstack 2
    .locals init (
        [0] string specificClass)
    L_0000: nop 
    L_0001: ldstr "======================== Do<T> ========================"
    L_0006: call void [mscorlib]System.Console::WriteLine(string)
    L_000b: nop 
    L_000c: ldarga.s a
    L_000e: constrained !!T
    L_0014: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
    L_0019: callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
    L_001e: stloc.0 
    L_001f: ldstr "======================== Do<T>: T = {0}"
    L_0024: ldloc.0 
    L_0025: call void [mscorlib]System.Console::WriteLine(string, object)
    L_002a: nop 
    L_002b: ldarga.s a
    L_002d: constrained !!T
    L_0033: callvirt instance void SimpleMethodInheritanceTest.A::f1()
    L_0038: nop 
    L_0039: ldarga.s a
    L_003b: constrained !!T
    L_0041: callvirt instance void SimpleMethodInheritanceTest.A::f2()
    L_0046: nop 
    L_0047: ret 
}

DoBBB Disassembled



The helper method that specifies class B as the explicit type of argument, works as expected (calling B::f2()). As we can see below in line 12, compiler emits IL that specifies B::f2 explicitly.
.method private hidebysig static void DoBBB(class SimpleMethodInheritanceTest.B a) cil managed
{
    .maxstack 8
    L_0000: nop 
    L_0001: ldstr "======================== DoBBB ========================"
    L_0006: call void [mscorlib]System.Console::WriteLine(string)
    L_000b: nop 
    L_000c: ldarg.0 
    L_000d: callvirt instance void SimpleMethodInheritanceTest.A::f1()
    L_0012: nop 
    L_0013: ldarg.0 
    L_0014: callvirt instance void SimpleMethodInheritanceTest.B::f2()
    L_0019: nop 
    L_001a: ret 
}

No comments :