Generic Methods Don't Call Methods of Type 'T'
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 us开发者_高级运维ing 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?
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
.
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.
Method hiding is not the same as polymorphism, as you've seen. You can always call the A version of the method simply by downcasting from B to A.
With a generic method, with T constrained to type A, there is no way for the compiler to know whether it could be some other type, so it would be very unexpected, in fact, for it to use a hiding method rather than the method defined on A. Method hiding is for convenience or interoperability; it has nothing to do with substituting behavior; for that, you need polymorphism and virtual methods.
EDIT:
I think the fundamental confusion here is actually Generics vs. C++ style templates. In .NET, there is only one base of code for the generic type. Creating a specialized generic type does not involve emitting new code for the specific type. This is different from C++, where a template specialization involves actually creating and compiling additional code, so that it will be truly specialized for the type specified.
The new
keyword is kind of a hack in C#. It contradicts polymorphism, because the method called depends on the type of the reference you hold.
精彩评论