【问题标题】:Overloading, generics and type constraints : method resolution重载、泛型和类型约束:方法解析
【发布时间】:2018-02-13 13:51:26
【问题描述】:

考虑这段代码 sn-p,具有泛型和重载函数:

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 的调用解析为调用,而不是“通用”函数所需的 callvirt。 从编译器的角度来看,我可以猜测 'U' 方法是一个更好的候选者,但我无法明确解释为什么......

我真的很想了解这里发生了什么,但我不知道。

但是,如果您考虑到 sn-p 的这个修改版本,它会变得更加奇怪,我们在其中引入了另一个级别的间接:

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# 编译器首选。

在泛型参数和继承方面,请有人解释重载方法解析的规则是什么?

【问题讨论】:

  • 显然,一旦你结合了继承和覆盖,编译器的函数解析变得奇怪:stackoverflow.com/a/48735728/3346583我假设接口的解析类似于继承(它毕竟是不允许多重继承的修复) .

标签: c# generics inheritance overloading


【解决方案1】:

这是在C# spec 中指定的。


在第一种情况下,我们有两个候选项:

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 更具体,因此选择了重载 2。


在第二种情况下,我们有以下两个候选项:

1. public static void OverloadedFunction(T (= V) a)
2. public static void OverloadedFunction(U a)

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

  • 根据 § 7.5.3.3,身份转换(VV)比任何其他类型的转换(例如扩大 VU)更好。
  • 因此,根据第 7.5.3.2 节,对于此调用,重载 1 比重载 2 是“更好的函数成员”。

【讨论】:

  • 感谢您的澄清。有关信息,我的代码在反射中破坏了 getmethod,说模棱两可的匹配异常:)
【解决方案2】:

这对我来说很有意义。

它将选择最佳匹配,优先选择不需要强制转换的重载。既然你用的是Class&lt;V&gt;,那么这个方法:

public static void OverloadedFunction(T a)

实际上变成:

public static void OverloadedFunction(V a)

当接受V 类型的参数时,这是一个更好的匹配,因为不需要强制转换。

在我看来,您的第一个示例更难以预测,因为任何一个都可以工作。但它似乎确实比泛型更喜欢强类型方法,我想这也是有道理的。

阅读规范,似乎首选非泛型方法:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#method-invocations

【讨论】:

    猜你喜欢
    • 2012-02-19
    • 1970-01-01
    • 2012-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多