参数重载明显二义性-仍然编译并运行?

24

我们在代码中发现了以下内容:

public static class ObjectContextExtensions
{

    public static T Find<T>(this ObjectSet<T> set, int id, params Expression<Func<T, object>>[] includes) where T : class
    {
        ...
    }

    public static T Find<T>(this ObjectSet<T> set, int id, params string[] includes) where T : class
    {
       ...
    }
}

正如您所看到的,这些签名都一样,除了params参数。

它们被用于多种方式之一:

DBContext.Users.Find(userid.Value); //userid being an int? (Nullable<int>)

奇怪的是,这个代码片段会调用第一个重载函数。

Q1: 为什么这不会产生编译错误?

Q2: 为什么C#编译器会将上述调用解析为第一个方法?

Edit: 澄清一下,这是C# 4.0,.Net 4.0和Visual Studio 2010。


3
最近,Eric Lippert的一篇帖子讨论了重载如何被解析:http://ericlippert.com/2013/12/23/closer-is-better/。我将为您进行翻译,并尽可能使其通俗易懂,但不改变原意。 - Katie Kilian
1
@CharlieKilian 这篇博客文章中没有关于声明顺序的内容。 - BartoszKP
1
有趣的事实:尽管这个代码可以运行,但是Resharper 7.1会将其视为错误。 - Yandros
1
似乎它认为泛型对象在参数中更“接近”。如果两者都是泛型或两者都不是泛型,则会抱怨模糊性。但我无法告诉你为什么,但我怀疑规范中的某些行说这就是它的方式。 - Chris
1
相关问题:https://dev59.com/KXPYa4cB1Zd3GeqPouu7 - leppie
显示剩余8条评论
3个回答

29

这显然是重载决议中的一个bug。

它在C# 5和C# 3中复现,但在Roslyn中没有;我不记得我们是否决定故意引入这个破坏性变化还是这是一个意外。(我现在的机器上没有C# 4,但是如果它在3和5中复制,则几乎肯定也会在4中出现。)

我已经向Roslyn团队的前同事提起了这件事。如果他们回复我有任何有趣的信息,我将更新这个答案。

由于我不再有C# 3 / 4 / 5源代码的访问权限,无法确定该错误的原因。考虑在connect.microsoft.com上报告它。

以下是一个简化的复制过程:

class P
{
    static void M(params System.Collections.Generic.List<string>[] p) {}
    static void M(params int[] p)  {}
    static void Main()
    {
        M();
    }
}

这似乎与元素类型的普适性有关。奇怪的是,正如Chris在他的答案中指出的那样,编译器选择了更通用的方法!我本来期望这个错误是相反的,即选择不太通用的方法。

顺便说一句,错误很可能是我的错,因为我在C# 3中做了大量的重载解析算法工作。对于这个错误表示歉意。

更新

我的Roslyn团队内部人士告诉我,这是长期以来存在的重载决策中已知的错误。实现了一个未被记录或证明的并规定较大的通用性是更好的类型的决定原则。这是一个毫无理由的荒谬规则,但它从产品中从未被移除。 Roslyn团队决定在这种情况下采取破坏性变化,并修复重载决策,以使其在这种情况下产生错误。(我不记得这个决定,但我们在这方面做了很多决定!)


2
谢谢你的回答。这对我们没有造成任何麻烦,我只是出于好奇在问。我很高兴自己正在为改进语言/编译器做出贡献 =) - Federico Berasategui
@HighCore:不用谢;感谢Chris让我注意到这个问题。当你将来升级时,如果Roslyn突然将其报告为错误,会给你带来麻烦吗? - Eric Lippert
不,我(以及我的团队)更喜欢编译器尽可能地捕获和消除歧义。我们可以通过添加重载T Find<T>(this ObjectSet<T> set, int id){...}来轻松解决这个问题。 - Federico Berasategui
@EricLippert:感谢您的反馈。我会尽量不习惯性地向您寻求帮助。:) - Chris

7

在IDEONE上编译器成功产生了一个错误。如果您逐步分析解析算法,它应该是一个错误:

1) 构造方法调用的候选方法集。从与M相关联的方法集开始,这些方法是通过先前的成员查找找到的[...] 集合缩减包括将以下规则应用于集合中的每个方法T.N,其中T是声明方法N的类型:

为简单起见,在此我们可以推断出该方法集包含您的两种方法。

然后进行缩减:

2) 如果N相对于A不可应用(第7.4.2.1节),则从集合中删除N。

这两种方法都适用于适用函数成员规则的扩展形式:

扩展形式是通过将函数成员声明中的参数数组替换为零个或多个元素类型的值参数来构建的,使得参数列表A中的参数数量与总参数数量匹配。如果A的参数少于函数成员声明中的固定参数数量,则无法构建函数成员的扩展形式,因此不适用。

该规则使得这两种方法都在缩减集合中。

实验(将一个或两个方法中的id参数类型更改为float)确认这两个函数仍然在候选集中,并且进一步通过隐式转换比较规则进行区分。

这表明上述算法在创建候选集方面表现良好,并且不依赖于某些内部方法排序。由于进一步区分方法的唯一因素是重载解析规则这似乎是一个错误,因为:

最佳函数成员是与给定参数列表相关时比所有其他函数成员都更好的函数成员,前提是每个函数成员都使用第7.4.2.2节中的规则与所有其他函数成员进行比较。

显然,这些方法中没有一种比另一种更好,因为这里不存在隐式转换。


让人困惑的是,它们的展开形式应该是完全相同的(它不添加任何参数,因此它们的展开形式与您已经注意到的相同),所以我不明白它在那之后怎么会有区别... - Chris
@Chris 请查看我提供的IDEONE链接。在MONO上似乎可以正常工作,因此这似乎是一个错误。 - BartoszKP
那绝对看起来像是一个 bug。唯一的问题是在哪个上面。我个人不相信自己能够完美地阅读规范及其所有复杂的语言。;-) 或者可能是某些东西已经改变了,我们正在比较不同的规范版本... 由于我刚意识到我还没有这样做,所以迟来的 +1。 - Chris

6
这并不是一个完整的答案,因为它只解释了两者之间的区别而没有解释原因。要完整,需要有规范参考。然而,我不想让我所做的研究在评论中丢失,所以会发布为答案。
两个重载之间的区别在于其中一个的参数是通用的,而另一个则不是。编译器似乎决定通用类型比非通用类型更接近。
也就是说,如果将Expression<...>类型改为int,编译器将抱怨二义性。如果类型都是通用的,则类似地抱怨二义性。
以下代码片段将更简单地展示这种行为:
void Main()
{
    TestMethod();
}

public void TestMethod(params string[] args)
{
    Console.WriteLine("NonGeneric");
}

public void TestMethod(params List<string>[] args)
{
    Console.WriteLine("Generic");
}

这段代码将会打印输出"Generic"。


这将违反C#规范如果MP是一个非泛型方法,而MQ是一个泛型方法,则MP比MQ更好。 - Yandros
我电脑上没有安装Office,所以我无法查看规范的副本,但是那段代码片段已经过测试,不是猜测的输出。然而,你的引用涉及到通用方法,而不是方法上的通用参数。另外,它必须是一个params类型的参数,在这里会有相关性…… - Chris
将另一个重载的参数类型更改为Action<T>不幸地并没有改变任何东西。 - BartoszKP
很遗憾,这并没有真正回答问题。 - ken2k
@ken2k:正如我在第一段中所说,我知道这只是一个不完整的答案,但至少它回答了“它选择具有通用类型参数的那个”这一点,即使它没有解释为什么编译器会选择这样做。 - Chris
5
这句话指的是方法的通用性,而不是形式参数类型。也就是说,如果我们有两个方法M(int)M<T>(T),其中Tint,那么非通用方法会胜出。请注意,这里的翻译使用了专业术语,但仍保持了原文的意思和结构。 - Eric Lippert

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