c#泛型和非泛型方法之间的重载决策

时间:2013-08-20 15:48:11

标签: c# generics overload-resolution

我在互联网和stackoverflow上做了一些基本的搜索,当涉及泛型版本方法和非泛型版本方法时,我看到了很多关于重载解析的讨论。我知道重载解析是在编译时完成的 - 因此如果我有这段代码:

public class A<T>
{
    public void DoStuff(T value)
    {
         InternalDoStuff(value);
    }

    protected void InternalDoStuff(int value)
    {
         Console.WriteLine("Non-generic version");
    }

    protected void InternalDoStuff(T value)
    {
         Console.WriteLine("Generic version");
    }

}

public class Test
{
    static void Main (string [] args)
    {
         A<int> a = new A<int> ();
         a.DoStuff(100);
    }
}

输出将是“通用版本”,因为“InternalDoStuff”的分辨率已由编译器整理出来,编译器看到的是“在DoStuff中使用T类型参数调用InternalDoStuff”。

但是我不知道这是否会有所不同:

public class B : A <int> 
{

}

public class Test
{
    static void Main (string [] args)
    {
         B b = new B ();
         b.DoStuff(100);
    }
}

现在我可以说编译器有足够的信息来决定“B是A的特定版本”,因此调用InternalDoStuff的非泛型版本吗?

分析这种过载分辨率是否有任何一般原则?

4 个答案:

答案 0 :(得分:4)

第二种方法在任何意义上都与第一种方法没有区别。

从A中派生B类绝不会改变为A类生成的IL代码.B只是继承这些方法。

如果查看A类的IL代码,可以看到它编译为调用泛型版本而不是非泛型版本 -

.method public hidebysig instance void DoStuff(!T 'value') cil managed
{
    .maxstack 8
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldarg.1 
    L_0003: call instance void
            ConsoleApplication1.A`1<!T>::InternalDoStuff(!0) <-- Generic version
    L_0008: nop 
    L_0009: ret 
}

来自Jon Skeet的文章here -

  

提醒一下,当你有两个时,会发生重载   具有相同名称但签名不同的方法。在编译时,   根据编译器,编译器确定它将调用哪一个   编译参数的时间类型和方法调用的目标。   (我假设你没有在这里使用动态,这使事情复杂化   有些。)

他提到使用动态延迟解析直到运行时。这段代码将为您的两种方法调用非泛型版本方法 -

public void DoStuff(T value)
{
   dynamic dynamicValue = value;
   InternalDoStuff(dynamicValue);
} 

请参阅Jon SkeetEric Lippert详细描述的答案。

答案 1 :(得分:2)

在C ++中,程序在执行期间可能创建的每种类型都必须在编译时生成。虽然C ++模板看起来像C#泛型,但它们的行为更类似于替换宏。因为编译器单独生成可能由泛型类型替换产生的每个类,所以它可以分别为每个类评估重载决策等内容。 C#泛型不能那样工作。

C#代码的编译分为两个阶段。第一阶段是在构建时完成的。处理该阶段的编译器获取源代码并将其转换为“通用中间语言”形式(相同的CIL形式用于VB.NET,F#等 - 因此名称)。源代码中的每个泛型类定义(例如List<T>)都以CIL形式生成一个类定义。在生成CIL之前,编译器会做出关于将应用哪些函数重载的所有决定。

稍后,当程序运行时,公共语言运行时将不会为程序可能想到的所有类生成代码,而是推迟为每个类生成代码,直到第一次实际使用它为止。在此步骤中,List<int>之类的内容会生成List<string>List<KeyValuePair<Dictionary<int,string>, Tuple<Cat,Dog>>>的不同机器代码。程序想要使用的一组可能类型不需要限制。可以在C#中合法地使用一种方法,在给定通用T的参数的情况下,将使用List<T>调用泛型方法(如果给定List<T>则会传递List<List<T>>如果给出会传递List<List<List<T>>>等等。如果事情嵌套得太深,程序可能会因OutOfMemoryException或类似的问题而死亡,但与C ++不同,程序可以生成的类型数量不需要在编译时受限;只有当程序试图实际使用太多不同的类型时才会出现问题。

CLR能够在生成代码时进行某些类型的通用替换,但它不处理重载解析(如上所述,在C#到CIL转换步骤中处理)。虽然在CLR中使用重载解析等功能可能会有一些优势,但它也会使CLR变得更加复杂。如果一个特别棘手的过载解决问题需要四分之一秒,这可能不是编译C#代码的问题,但是对于这样的事情停止运行时间为四分之一是不可取的。

答案 2 :(得分:1)

enter image description here这将调用“非泛型”版本:

public class A<T>
{
    public virtual void DoStuff(T value)
    {
        InternalDoStuff(value);
    }

    protected void InternalDoStuff(int value)
    {
        Console.WriteLine("Non-generic version");
    }

    protected void InternalDoStuff(T value)
    {
        Console.WriteLine("Generic version");
    }

}
public class B : A<int>
{
    public override void DoStuff(int value)
    {
        InternalDoStuff(value);
    }
}

答案 3 :(得分:1)

InternalDoStuffDoStuff的调用在A<T>编译时受到约束。调用来自B实例的事实不会以任何方式影响重载决策。

在汇编DoStuff时,有2个InternalDoStuff成员可供选择

  • InternalDoStuff(T value)
  • InternalDoStuff(int value)

DoStuff方法正在传递T值,因此int的重载无效。因此,只有一个适用的成员InternalDoStuff(T),编译器会选择这个成员。