为啥在存在非泛型时选择泛型方法?

Posted

技术标签:

【中文标题】为啥在存在非泛型时选择泛型方法?【英文标题】:Why is a generic method chosen when a non-generic exists?为什么在存在非泛型时选择泛型方法? 【发布时间】:2015-09-17 21:34:50 【问题描述】:

下面的程序产生这个输出:

Foo<T> called

Process is terminated due to ***Exception.

所以,Foo(baz) 调用泛型 Foo&lt;T&gt;,但 Bar(baz) 递归并且调用Bar&lt;T&gt;

我使用 C# 5.0 和 Microsoft .NET。当非泛型方法是override 时,编译器似乎选择了泛型方法,而不是递归。

我在哪里可以找到这个规则的解释?(我猜到编译器在这两种情况下都会选择递归。)

这是整个程序:

using System;

namespace ConsoleApplication1 
    class Baz  

    abstract class Parent 
        public abstract void Foo(Baz baz);
    

    class Child : Parent 
        void Bar<T>(T baz) 
            Console.WriteLine("Bar<T> called");
        

        public void Bar(Baz baz) 
            Bar(baz);
        

        void Foo<T>(T baz) 
            Console.WriteLine("Foo<T> called");
        

        public override void Foo(Baz baz) 
            Foo(baz);
        
    

    class Program 
        static void Main(string[] args) 
            var child = new Child();
            child.Foo(null);
            child.Bar(null);
            Console.ReadLine();
        
    

【问题讨论】:

我喜欢将所有此类情况视为编译器创建者惩罚您使用继承的方式...... 似乎子类中的非覆盖方法优先于子类中的覆盖方法;它可能会将被覆盖的方法视为父方法的一部分。 【参考方案1】:

根据 MSDN 文档,优先考虑未被覆盖的方法签名。由于 Foo 的非泛型版本被覆盖,它立即进入选择方法的优先级底部。一般来说,下一步是选择最具体的方法并执行它。对于 Bar 方法,Bar(Baz baz) 方法将始终是您的情况最具体的方法。

重载解析是一种用于选择最佳的编译时机制 函数成员调用给定的参数列表和一组 候选功能成员。过载分辨率选择功能 在 C# 中的以下不同上下文中调用的成员:

调用在调用表达式中命名的方法(第 7.5.5)。调用以对象创建表达式命名的实例构造函数(第 7.5.10.1 节)。 通过元素访问调用索引器访问器(第 7.5.6 节)。调用表达式中引用的预定义或用户定义的运算符 (第 7.2.3 节和第 7.2.4 节)。

这些上下文中的每一个都定义了 一组候选函数成员和它自己的参数列表 独特的方式,如上面列出的部分中详细描述的那样。 为 例如,方法调用的候选集不 包括标记为覆盖的方法(第 7.3 节),以及基中的方法 如果派生类中的任何方法是 适用(第 7.5.5.1 节)。

MSDN Overload Resolution

我将我认为与您的问题相关的文字加粗。

Stack Overflow 上的另一个问题可能会有所帮助。它一般谈论方法解析。不涉及被覆盖的方法,但有助于填补一些我没有涉及的过程。

【讨论】:

不错,不用看msdn我就知道原因了!【参考方案2】:

重载解析向上搜索继承链,在每个点查找定义的方法。

Child 定义了void Foo&lt;T&gt;(T baz),但没有定义void Foo(Baz baz),因此选择了void Foo&lt;T&gt;(T baz)

通常这是有道理的;在实际代码中,如果Foo&lt;T&gt;(T baz) 与基类中的Foo(Baz baz) 在传递Baz 时所做的工作不相似,那么您的设计会令人困惑,您应该选择一个新名称。

您可以使用public new void Foo(Baz baz)public new virtual void Foo(Baz baz) 来强制在Child 中定义覆盖(尽管这里需要在层次结构中设置一个中间步骤,以便抽象方法有一个实现)可以调用base.Foo(baz)(调用基本实现)和/或Foo&lt;Baz&gt;(baz)(调用通用版本)`,但最好避免这种技巧。

【讨论】:

【参考方案3】:

也许它的行为就像你实现这样的东西时

    void myMethod(long? l)  
    void myMethod(int? i)  

使用null 调用它将使用int?

添加这个

    void myMethod(short? i)  

仍然使用null 调用它,代码将切换到short?

可能有内部订单/优先级正在执行?

现在有了你的代码,我给出这个只是为了说明让编译器决定和程序员决定(显式调用)之间的区别

这是通用的Baz

.method private hidebysig 
    instance void Bar<T> (
        !!T baz
    ) cil managed 

    // Method begins at RVA 0x2060
    // Code size 13 (0xd)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldstr "Bar<T> called"
    IL_0006: call void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret
 // end of method Child::Bar

你的实现

    public void Bar(Baz baz) 
        Bar(baz);
    

给这个

.method public hidebysig 
    instance void Bar (
        class ConsoleApplication1.Baz baz
    ) cil managed 

    // Method begins at RVA 0x206e
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldarg.1
    IL_0003: call instance void ConsoleApplication1.Child::Bar(class ConsoleApplication1.Baz)
    IL_0008: nop
    IL_0009: ret
 // end of method Child::Bar

这个

    public void Bar(Baz baz)
    
        Bar<Baz>(baz);
    

给这个

.method public hidebysig 
    instance void Bar (
        class ConsoleApplication1.Baz baz
    ) cil managed 

    // Method begins at RVA 0x206e
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldarg.1
    IL_0003: call instance void ConsoleApplication1.Child::Bar<class ConsoleApplication1.Baz>(!!0)
    IL_0008: nop
    IL_0009: ret
 // end of method Child::Bar

【讨论】:

以上是关于为啥在存在非泛型时选择泛型方法?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C# 无法从非泛型静态方法的签名推断泛型类型参数类型?

为啥为非泛型方法或构造函数提供显式类型参数会编译?

从非泛型类调用抽象泛型类中定义的方法的最佳方法是啥

对于泛型+非泛型,是不是/为啥需要从两次继承 WebViewPage?

ObjectDataSource 未能找到带参数的非泛型方法

泛型与非泛型重载调用