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
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 :
Post a Comment