为什么添加一个方法会导致模糊的调用,即使它不参与到歧义中?

113

我有这个类

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] something)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }
}

如果我像这样调用它:

        var blah = new Overloaded();
        blah.ComplexOverloadResolution("Which wins?");

它会在控制台输出Normal Winner

但是,如果我添加另一个方法:

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }

我遇到了以下错误:

调用 'Overloaded.ComplexOverloadResolution(params string[])' 和 'Overloaded.ComplexOverloadResolution<string>(string)' 这两个方法或属性存在歧义。

我可以理解添加一个方法可能会导致调用不明确,但这是在已经存在的两种方法(params string[])<string>(string)之间产生歧义!显然,歧义中涉及的两个方法都不是新添加的方法,因为第一个是params,第二个是泛型。

这是一个bug吗?规范的哪一部分说明应该是这种情况?


2
我认为'Overloaded.ComplexOverloadResolution(string)'并不是指<string>(string)方法,而是指没有提供对象的(string, object)方法。 - phoog
1
@phoog哦,那个数据被StackOverflow切掉了,因为它是一个标签,但错误消息有模板设计器。我正在把它加回去。 - McKay
你抓住我了!我在回答中引用了规范的相关部分,但我并没有花上半个小时去阅读和理解它们! - phoog
@phoog,我相信C#规范有这样的剪刀石头布案例(@EricLippert称之为不可传递的更优关系),但在这种情况下,编译器认为它遇到了这样的情况,但实际上并没有。因此出现了一个错误。 - McKay
啊,对了,我忘记了故事的关键部分 :-) - phoog
显示剩余2条评论
5个回答

108

这是一个bug吗?

是的。

恭喜你,你发现了重载决策中的一个bug。该bug在C# 4和5中复现;但在语义分析器的“Roslyn”版本中不会复现。我已经通知了C# 5测试团队,希望我们能在最终发布之前调查和解决这个问题。(像往常一样,不做任何承诺。)

下面是正确的分析。候选项如下:

0: C(params string[]) in its normal form
1: C(params string[]) in its expanded form
2: C<string>(string) 
3: C(string, object) 

候选项零显然不适用,因为 string 无法转换为 string[]。剩下三个。
在这三个中,我们必须确定一个最佳的方法。我们通过对剩余的三个候选项进行成对比较来实现这一点。有三个这样的对。一旦我们剥离省略的可选参数,它们所有的参数列表都是相同的,这意味着我们必须去参考规范第7.5.3.2节中描述的高级排位赛。
哪个更好,1还是2?相关的决胜者规则是:泛型方法总是比非泛型方法差。2比1更差。所以2不能成为赢家。
哪个更好,1还是3?相关的决胜者规则是:只适用于其展开形式的方法始终比适用于其正常形式的方法差。因此1比3更差。所以1不能成为赢家。
哪个更好,2还是3?相关的决胜者规则是:泛型方法总是比非泛型方法差。2比3更差。所以2不能成为赢家。

从多个适用的候选人中选择一个候选人,该候选人必须(1)无敌,(2)击败至少另一个候选人,并且(3)是具有前两个属性的唯一候选人。候选人三没有被任何其他候选人打败,并且至少击败了另一个候选人;它是唯一具有此属性的候选人。因此,候选人三是独一无二的最佳候选人。它应该获胜。

不仅是C# 4编译器出错了,正如你所正确指出的那样,它还报告了一个奇怪的错误消息。编译器在重载分析上出错有点令人惊讶。它在错误消息上出错完全不足为奇;如果无法确定最佳方法,则“模棱两可的方法”错误启发式基本上从候选集中选择任意两个方法。它不能很好地找到“真正”的歧义,如果实际上存在歧义。

有人可能会合理地问为什么会这样。找到“明显含糊不清”的两种方法相当棘手,因为“更好”关系是“不可传递的”。可能会出现这样的情况:候选1比2更好,2比3更好,3比1更好。在这种情况下,我们无法做得比选择其中两个作为“含糊不清的”更好。
我想改进Roslyn的这种启发式方法,但这是一个低优先级的任务。
(读者练习:“设计一种线性时间算法,以识别n个元素集合中唯一最佳成员,其中更好关系是不可传递的”是我面试这个团队的问题之一。这不是一个非常难的算法;试试看吧。)
我们对将可选参数添加到C#中推迟了很长时间的原因之一是它引入了许多复杂的含糊不清的情况到重载解析算法中;显然我们没有做对。
如果您想输入Connect问题以跟踪它,请随意。如果你只是想引起我们的注意,那就可以了。我将在明年进行测试。
感谢您提醒我。对于错误,我表示歉意。

1
谢谢您的回复。您说“1比2更差”,但如果我只有方法1和2,它会选择方法1吗? - McKay
啊,是的,这更清晰了。谢谢。实际上我们并没有遇到这个问题,但我为我的同事们制作了一个C#难题,并发现了这个奇怪的问题。这个错误并不是真正的问题。 - McKay
1
考虑到这一年已经不到半周了,阅读“今年的其余时间”这个短语感觉有些尴尬 :) - BoltClock
2
@BoltClock 的确,"离开一整年"这个说法意味着休息一天。 - phoog
1
我认为是这样的。我把“3)成为具有前两个属性的唯一候选人”解读为“是唯一一个(不败且至少击败了另一个候选人)的候选人”。但是你最近的评论让我想到“(是唯一一个不败的)并且至少击败了另一个候选人”的意思。英语确实需要分组符号。如果后者是真的,那么我又明白了。 - default.kramer
显示剩余3条评论

5
什么部分规范说明应该是这种情况?
第7.5.3节(重载决议)以及第7.4节(成员查找)和第7.5.2节(类型推断)。
特别需要注意的是第7.5.3.2节(更好的函数成员),其中部分内容为“没有相应参数的可选参数将从参数列表中删除”,“如果M(p)是非泛型方法,而M(q)是泛型方法,则M(p)优于M(q)”。
但是,我不充分了解规范的这些部分,无法判断这种行为受到哪些规范控制,更不用说它是否符合规范。

但这并不能解释为什么添加一个成员会导致两个已经存在的方法之间产生歧义。 - McKay
@McKay 好的,没问题(见编辑)。我们只能等待Eric Lippert告诉我们这是否是正确的行为:-> - phoog
1
这些规范的确是正确的部分。问题在于它们说这不应该是这样的! - Eric Lippert

3

通过更改某些方法中第一个参数的名称并指定要分配的参数,您可以避免这种歧义。

像这样:

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] somethings)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Overloaded a = new Overloaded();
        a.ComplexOverloadResolution(something:"asd");
    }
}

哦,我知道这段代码很糟糕,有几种方法可以解决它,但问题是“编译器为什么会表现出这种行为?” - McKay

1

如果您从第一个方法中删除params,就不会发生这种情况。您的第一个和第三个方法都有有效的调用ComplexOverloadResolution(string),但如果您的第一个方法是public void ComplexOverloadResolution(string[] something),那么就不会有歧义。

为参数object somethingElse = null提供值将使其成为可选参数,因此在调用该重载时没有必要指定它。

编辑:编译器在这里做了一些疯狂的事情。如果您将第三个方法移到第一个方法之后的代码中,则会正确报告。因此,它似乎正在采用前两个重载并报告它们,而不检查正确的重载。

'ConsoleApplication1.Program.ComplexOverloadResolution(params string[])'和 'ConsoleApplication1.Program.ComplexOverloadResolution(string, object)'

编辑2:新发现。从上述三个中删除任何一个方法都不会在两者之间产生歧义。因此,似乎只有存在三个方法时才会出现冲突,无论顺序如何。


歧义发生在你的第一个和第三个方法之间,但编译器为什么报告另外两个我不知道。 - Tomislav Markovski
但是如果我删除第二个方法,就没有歧义了,它会成功调用第三个方法。因此,编译器似乎没有在第一和第三个方法之间产生歧义。 - McKay
看我的编辑。疯狂的编译器。 - Tomislav Markovski
任意两个函数重载不会产生歧义,但三个函数可能会;更好的关系是不可传递的。 - Eric Lippert
@TomislavMarkovski: 石头剪刀布 - phoog
显示剩余3条评论

1
  1. 如果你写

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");
    

    或者只是写

    var blah = new Overloaded();
    blah.ComplexOverloadResolution();
    

    它最终会进入相同的方法,在方法中

    public void ComplexOverloadResolution(params string[] something
    

    这是因为 params 关键字使其成为了最佳匹配,即使没有指定参数

  2. 如果你尝试像这样添加新方法

    public void ComplexOverloadResolution(string something)
    {
        Console.WriteLine("Added Later");
    }
    

    它将完美编译并调用此方法,因为它是与带有 string 参数的调用的完美匹配。比 params string[] something 更强大。

  3. 你像这样声明第二个方法

    public void ComplexOverloadResolution(string something, object something=null);
    

    编译器在第一个方法和这个方法之间陷入了彻底的混乱,只是添加了一个方法。 因为它不知道现在应该调用哪个函数

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");
    

    实际上,如果你从调用中删除字符串参数,就像下面的代码一样,一切都会正确编译并像以前一样工作

    var blah = new Overloaded();
    blah.ComplexOverloadResolution(); // 将调用 ComplexOverloadResolution(params string[] something) 函数,作为最佳匹配。
    

首先,您编写了两个相同的调用案例。因此,它当然会进入相同的方法,或者您是想编写不同的内容吗? - McKay
但是,如果我正确理解您回答的其余部分,您并未阅读编译器指出的混淆问题,即第一个和第二个方法之间的混淆,而不是我刚添加的新的第三个方法。 - McKay
啊,谢谢。但是这仍然存在我在你的帖子中提到的第二个评论中提到的问题。 - McKay
更明确地说,你说“编译器在第一个方法和这个方法之间跳来跳去,只是添加了一个方法。”但实际上不是这样的。它会跳到另外两个方法:params方法和泛型方法。 - McKay
@McKay:嗯,如果有三个函数的state(状态),而不是一个或两个,那么就会感到困惑。实际上,只需要注释掉其中任何一个函数即可解决问题。可用函数中最佳匹配的是具有params的那个函数,第二个是具有泛型参数的那个函数,当我们添加第三个时,它会在函数集合中造成混淆。我认为,很可能是编译器产生了不清晰的错误消息。 - Tigran

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