通用方法不要调用类型'T'的方法

时间:2011-01-27 15:35:13

标签: c# .net generics

假设我有两个类:

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"
}

似乎编译器正在生成对约束中指定的基类的方法调用,所以我想我明白发生了什么,但这不是我所期望的。这真的是正确的结果吗?

4 个答案:

答案 0 :(得分:7)

泛型不是C ++模板

泛型是一种通用类型:编译器只输出一个泛型类(或方法)。泛型不能通过编译时将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的致电就是一个完美的例子,简单的表达方式为IDisposableActivator.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#中是一种黑客攻击。它与多态性相矛盾,因为调用的方法取决于您持有的引用类型。