Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Methods Don't Call Methods of Type 'T'

Tags:

c#

.net

generics

Suppose I have two classes:

class a
{
    public void sayGoodbye() { Console.WriteLine("Tschüss"); }
    public virtual void sayHi() { Console.WriteLine("Servus"); }
}

class b : a
{
    new public void sayGoodbye() { Console.WriteLine("Bye"); }
    override public void sayHi() { Console.WriteLine("Hi"); }
}

If I call a generic method that requires type 'T' to be derived from class 'a':

void call<T>() where T : a

Then inside that method I call methods on an instance of type 'T' the method call are bound to type 'a', as if the instance was being cast as 'a':

call<b>();
...
void call<T>() where T : a
{
    T o = Activator.CreateInstance<T>();
    o.sayHi(); // writes "Hi" (virtual method)
    o.sayGoodbye(); // writes "Tschüss"
}

By using reflection I am able to get the expected results:

call<b>();
...
void call<T>() where T : a
{
    T o = Activator.CreateInstance<T>();
    // Reflections works fine:
    typeof(T).GetMethod("sayHi").Invoke(o, null); // writes "Hi"
    typeof(T).GetMethod("sayGoodbye").Invoke(o, null); // writes "Bye"
}

Also, by using an interface for class 'a' I get the expected results:

interface Ia
{
    void sayGoodbye();
    void sayHi();
}
...
class a : Ia // 'a' implements 'Ia'
...
call<b>();
...
void call<T>() where T : Ia
{
    T o = Activator.CreateInstance<T>();
    o.sayHi(); // writes "Hi"
    o.sayGoodbye(); // writes "Bye"
}

The equivalent non-generic code also works fine:

call();
...
void call()
{
    b o = Activator.CreateInstance<b>();
    o.sayHi(); // writes "Hi"
    o.sayGoodbye(); // writes "Bye"
}

Same thing if I change the generic constraint to 'b':

call<b>();
...
void call<T>() where T : b
{
    T o = Activator.CreateInstance<T>();
    o.sayHi(); // writes "Hi"
    o.sayGoodbye(); // writes "Bye"
}

It seems that the compiler is generating method calls to the base class specified in the constraint, so I guess I understand what is happening, but this is not what I expected. Is this really the correct result?

like image 934
Paul Avatar asked Jan 27 '11 15:01

Paul


2 Answers

Generics aren't C++ Templates

Generics are a general type: there will be only one generic class (or method) output by the compiler. Generics doesn't work by compile-time replacing T with the actual type provided, which would require compiling a separate generic instance per type parameter but instead works by making one type with empty "blanks". Within the generic type the compiler then proceeds to resolve actions on those "blanks" without knowledge of the specific parameter types. It thus uses the only information it already has; namely the constraints you provide in addition to global facts such as everything-is-an-object.

So when you say...

void call<T>() where T : a {
    T o = Activator.CreateInstance<T>();
    o.sayGoodbye();//nonvirtual

...then type T of o is only relevant at compile time - the runtime type may be more specific. And at compile time, T is essentially a synonym for a - after all, that's all the compiler knows about T! So consider the following completely equivalent code:

void call<T>() where T : a {
    a o = Activator.CreateInstance<T>();
    o.sayGoodbye();//nonvirtual

Now, calling a non-virtual method ignores the run-time type of a variable. As expected, you see that a.sayGoodbye() is called.

By comparison, C++ templates do work the way you expect - they actually expand the template at compile time, rather than making a single definition with "blanks", and thus the specific template instances can use methods only available to that specialization. As a matter of fact, even at run-time, the CLR avoids actually instantiating specific instances of templates: since all the calls are either virtual (making explicit instantiation unnecessary) or non-virtual to a specific class (again, no point in instantiating), the CLR can use the same bytes - probably even the same x86 code - to cover multiple types. This isn't always possible (e.g. for value types), but for reference types that saves memory and JIT-time.

Two more things...

Firstly, your call method uses Activator - that's not necessary; there's an exceptional constraint new() you may use instead that does the same thing but with compile-time checking:

void call<T>() where T : a, new() {
    T o = new T();
    o.sayGoodbye();

Attempting to compile call<TypeWithoutDefaultConstructor>() will fail at compile time with human-readable message.

Secondly, it may seem as though generics are largely pointless if they're just blanks - after all, why not simply work on a-typed variables all along? Well, although at compile-time you can't rely on any details a sub-class of a might have within the generic method, you're still enforcing that all T are of the same subclass, which allows in particular the usage of the well-known containers such as List<int> - where even though List<> can never rely on int internals, to users of List<> it's still handy to avoid casting (and related performance and correctness issues).

Generics also allow richer constraints than normal parameters: for example, you can't normally write a method that requires its parameter to be both a subtype of a and IDisposable - but you can have several constraints on a type parameter, and declare a parameter to be of that generic type.

Finally, generics may have run-time differences. Your call to Activator.CreateInstance<T>() is a perfect illustration of that, as would be the simple expression typeof(T) or if(myvar is T).... So, even though in some sense the compiler "thinks" of the return type of Activator.CreateInstance<T>() as a at compile time, at runtime the object will be of type T.

like image 160
Eamon Nerbonne Avatar answered Oct 08 '22 01:10

Eamon Nerbonne


sayGoodbye is not virtual.

The compiler only "knows" T is of type a. It will call sayGoodbye on a.

On type b you redefine sayGoodbye, but the compiler is not aware of type b. It cannot know all derivates of a. You can tell the compiler that sayGoodbye may be overriden, by making it virtual. This will cause the compiler to call sayGoodbye on a special way.

like image 30
GvS Avatar answered Oct 07 '22 23:10

GvS