重载、泛型和类型约束:方法解析

4
考虑这个代码片段,其中包含泛型和重载函数:
using System;

namespace Test_Project
{
    public interface Interface
    {
        void f();
    }

    public class U : Interface
    {
        public void f() {}
    }

    public class Class<T> where T: Interface
    {
        public static void OverloadedFunction(T a)
        {
            Console.WriteLine("T");
            a.f();
        }

        public static void OverloadedFunction(U a)
        {
            Console.WriteLine("U");
            a.f();
        }
    }

    class Program
    {
        public static void Invoke(U instance)
        {
            Class<U>.OverloadedFunction(instance);
        }

        static void Main(string[] args)
        {
            Invoke(new U());
        }
    }
}

我认为它不会编译,因为我有两个适合OverloadedFunction的候选方法,但它确实编译并打印出“U”。
在生成的IL中,我可以看到:
.method public hidebysig static 
    void Invoke (
        class Test_Project.U 'instance'
    ) cil managed 
{
    // Method begins at RVA 0x2085
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: call void class Test_Project.Class`1<class Test_Project.U>::OverloadedFunction(class Test_Project.U)
    IL_0006: ret
} // end of method Program::Invoke

意味着C#编译器将对OverloadedFunction的调用解析为一次调用,而不是所需的“通用”函数调用调用virt。我可以猜测从编译器的角度来看,“U”方法是更好的选择,但我无法确切地解释为什么...
我真的很想理解这里发生了什么,但我一点头绪也没有。
但如果考虑这个修改后的片段,引入另一个间接层级,情况变得更加奇怪:
using System;

namespace Test_Project
{
    public interface Interface
    {
        void f();
    }

    public class U : Interface
    {
        public void f() {}
    }

    public class V : U { }

    public class Class<T> where T: Interface
    {
        public static void OverloadedFunction(T a)
        {
            Console.WriteLine("T");
            a.f();
        }

        public static void OverloadedFunction(U a)
        {
            Console.WriteLine("U");
            a.f();
        }
    }

    class Program
    {
        public static void Invoke(V instance)
        {
            Class<V>.OverloadedFunction(instance);
        }

        static void Main(string[] args)
        {
            Invoke(new V());
        }
    }
}

我希望这个程序仍然打印出 'U',因为 'V' 是通过继承与 'U' 相关的。但是它打印出 'T',如 MSIL 所示:
.method public hidebysig static 
    void Invoke (
        class Test_Project.V 'instance'
    ) cil managed 
{
    // Method begins at RVA 0x208d
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: call void class Test_Project.Class`1<class Test_Project.V>::OverloadedFunction(!0)
    IL_0006: ret
} // end of method Program::Invoke

这意味着C#编译器更喜欢使用通用版本。

请问有人能解释一下在涉及泛型参数和继承时,方法重载的解析规则是什么吗?


显然,一旦你结合了继承和覆盖,编译器的函数解析就变得奇怪了:https://dev59.com/aajka4cB1Zd3GeqPG_g7#48735728 我认为接口的解析方式类似于继承(毕竟这是不允许多重继承的修复方法)。 - Christopher
2个回答

5
这在C#规范中有明确说明。
在第一种情况下,我们有两个候选者:
1. public static void OverloadedFunction(T (= U) a)
2. public static void OverloadedFunction(U a)

规范的§7.5.3.6节提到(我强调):
尽管声明签名必须是唯一的,但是替换类型参数可能导致相同的签名。 在这种情况下,上面的重载解析的决定规则将选择最具体的成员。
而解决冲突的规则(§7.5.3.2)如下:
类型参数比非类型参数不具体。
其中T是类型参数;U不是。 因此,U更具体,选择第二个重载方法。
在第二种情况中,我们有以下两个候选项:
1. public static void OverloadedFunction(T (= V) a)
2. public static void OverloadedFunction(U a)

在这里,重载1 T(= V) 是更好的匹配:

  • 根据 § 7.5.3.3,身份转换(VV)比任何其他类型的转换(如将 V 扩展到 U)更好。
  • 因此,根据 § 7.5.3.2,对于此调用,重载1比重载2更好。

感谢澄清。顺便提一下,我的代码在反射中破坏了 getmethod,导致出现了模棱两可的匹配异常 :) - Regis Portalez

0

我理解这个意思。

它会选择最佳匹配,优先选择不需要转换的重载。由于您正在使用 Class<V>,因此该方法如下:

public static void OverloadedFunction(T a)

有效地变成了:

public static void OverloadedFunction(V a)

当接受类型为V的参数时,哪个更匹配呢?因为不需要转换。

在我看来,你的第一个例子更加不可预测,因为两者都可以工作。但它似乎更喜欢强类型方法而不是通用方法,这也是有道理的。

阅读规范,似乎非泛型方法更受青睐: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#method-invocations


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接