假设我有两个类:
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"); }
}
如果我调用一个通用方法,要求类型'T'从类'a'派生:
void call<T>() where T : a
然后在该方法中我调用类型为'T'的实例上的方法,方法调用绑定为'a',就像实例被转换为'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"
}
通过使用反射,我能够得到预期的结果:
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"
}
此外,通过使用类'a'的接口,我得到了预期的结果:
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"
}
等效的非通用代码也可以正常工作:
call();
...
void call()
{
b o = Activator.CreateInstance<b>();
o.sayHi(); // writes "Hi"
o.sayGoodbye(); // writes "Bye"
}
如果我将通用约束更改为'b',则相同:
call<b>();
...
void call<T>() where T : b
{
T o = Activator.CreateInstance<T>();
o.sayHi(); // writes "Hi"
o.sayGoodbye(); // writes "Bye"
}
似乎编译器正在生成对约束中指定的基类的方法调用,所以我想我明白发生了什么,但这不是我所期望的。这真的是正确的结果吗?
答案 0 :(得分:7)
泛型是一种通用类型:编译器只输出一个泛型类(或方法)。泛型不能通过编译时将T
替换为提供的实际类型,这将需要为每个类型参数编译单独的通用实例,而是通过使一个类型为空“空白”来工作”。在泛型类型中,编译器然后在不知道特定参数类型的情况下继续解析对那些“空白”的动作。因此它使用了它已有的唯一信息;即除了全局事实之外你提供的约束,例如所有东西都是对象。
所以当你说...
void call<T>() where T : a {
T o = Activator.CreateInstance<T>();
o.sayGoodbye();//nonvirtual
...然后T
的{{1}}类型仅在编译时相关< - 运行时类型可能更具体。在编译时,o
本质上是T
的同义词 - 毕竟,所有编译器都知道a
!因此,请考虑以下完全等效的代码:
T
现在,调用非虚方法会忽略变量的运行时类型。正如预期的那样,您会看到void call<T>() where T : a {
a o = Activator.CreateInstance<T>();
o.sayGoodbye();//nonvirtual
被调用。
相比之下,C ++模板做以您期望的方式工作 - 它们实际上是在编译时扩展模板,而不是使用“空白”进行单一定义,因此特定模板实例可以使用仅适用于该专业化的方法。事实上,即使在运行时,CLR也避免实际实例化模板的特定实例:因为所有调用都是虚拟的(不需要显式实例化)或非虚拟特定类(同样,实例化中没有任何意义) ),CLR可以使用相同的字节 - 甚至可能使用相同的x86代码 - 来覆盖多种类型。这并不总是可行的(例如对于值类型),但对于节省内存和JIT时间的引用类型。
首先,您的通话方法使用a.sayGoodbye()
- 这不是必需的;有一个特殊的约束Activator
你可以使用它做同样的事情但是使用编译时检查:
new()
尝试编译void call<T>() where T : a, new() {
T o = new T();
o.sayGoodbye();
将在编译时失败并显示人类可读的消息。
其次,如果它们只是空白,那么仿制药看起来似乎毫无意义 - 毕竟,为什么不简单地处理call<TypeWithoutDefaultConstructor>()
类型的变量呢?好吧,虽然在编译时你不能依赖a
的子类可能在泛型方法中的任何细节,但你仍然强制执行所有{{1}是相同的子类,它特别允许使用众所周知的容器,例如a
- 尽管T
永远不能依赖List<int>
} internals,对于List<>
的用户来说,避免转换(以及相关的性能和正确性问题)仍然很方便。
泛型也允许比普通参数更丰富的约束:例如,您通常不能编写一个方法,要求其参数既是int
的子类型又是List<>
- 但您可以对类型参数有几个约束,并将参数声明为该泛型类型。
最后,泛型可能存在运行时差异。您对a
的致电就是一个完美的例子,简单的表达方式为IDisposable
或Activator.CreateInstance<T>()
。因此,即使在某种意义上编译器在编译时“认为”typeof(T)
的返回类型为if(myvar is T)...
,但在运行时该对象的类型为Activator.CreateInstance<T>()
。
答案 1 :(得分:3)
sayGoodbye不是虚拟的。
编译器只“知道”T是类型a。它会在一个叫做goodGoodbye的地方。
在类型b上重新定义sayGoodbye,但编译器不知道类型b。它无法知道a的所有衍生物。您可以告诉编译器,通过使其成为虚拟,可以覆盖sayGoodbye。这将导致编译器以特殊方式调用sayGoodbye。
答案 2 :(得分:3)
隐藏方法与多态性不同,正如您所见。您可以随时通过从B向下转换为A来调用方法的A版本。
使用泛型方法,将T约束为类型A,编译器无法知道它是否可能是其他类型,因此实际上,它会非常意外地使用隐藏方法方法隐藏是为了方便或互操作;它与替代行为无关;为此,你需要多态和虚方法。
编辑:
我认为这里的根本混淆实际上是泛型与C ++样式模板。在.NET中,泛型类型只有一个代码库。创建专用泛型类型不涉及为特定类型发出新代码。这与C ++不同,其中模板专门化涉及实际创建和编译其他代码,因此它将真正专门用于指定的类型。
答案 3 :(得分:2)
new
关键字在C#中是一种黑客攻击。它与多态性相矛盾,因为调用的方法取决于您持有的引用类型。