为什么 Func<T> 和 Func<IEnumerable<T>> 会产生歧义?

21

这个问题让我困惑不已,所以我想在这里问一下,希望有C#大神可以向我解释。

为什么这段代码会生成一个错误?

class Program
{
    static void Main(string[] args)
    {
        Foo(X); // the error is on this line
    }

    static String X() { return "Test"; }

    static void Foo(Func<IEnumerable<String>> x) { }
    static void Foo(Func<String> x) { }
}

这个错误:

Error
    1
    The call is ambiguous between the following methods or properties:
'ConsoleApplication1.Program.Foo(System.Func<System.Collections.Generic.IEnumerable<string>>)' and 'ConsoleApplication1.Program.Foo(System.Func<string>)'
    C:\Users\mabster\AppData\Local\Temporary Projects\ConsoleApplication1\Program.cs
    12
    13
    ConsoleApplication1
无论我使用哪种类型 - 如果您在该代码中将"String"声明替换为"int",您将获得同样的错误。就像编译器无法区分 Func<T>Func<IEnumerable<T>> 之间的区别一样。请问有人能解释一下吗?
2个回答

28

好的,这是交易。

简短的版本:

  • 令人惊讶的是,模棱两可的错误实际上是正确的。
  • C# 4编译器在正确的模棱两可错误之后还会产生虚假的错误。这似乎是编译器中的一个错误。

详细版:

我们有一个重载决策问题。重载决策非常明确规定。

第一步:确定候选集。这很容易。候选者是Foo(Func<IEnumerable<String>>)Foo(Func<String>)

第二步:确定候选集的哪些成员是“适用”的。适用的成员具有每个参数类型可转换为每个参数类型。

Foo(Func<IEnumerable<String>>)是否适用?嗯,X 是否转换为 Func<IEnumerable<String>

我们参考规范的第6.6节。规范的这部分内容是我们语言设计师所谓的“非常奇怪”的部分。基本上,它说可以存在转换,但使用该转换是错误的。(我们之所以有这种奇怪的情况,主要是为了避免未来的破坏性更改和避免“鸡和蛋”情况,但在您的情况下,我们得到了一些不幸的行为结果。)

基本上,在此处的规则是,如果以 X() 形式调用的重载决策将成功,则从 X 到没有参数的委托类型的转换存在。显然,这样的调用成功,因此存在一种转换。实际上使用该转换是错误的,因为返回类型不匹配,但重载决策总是忽略返回类型

因此,存在从 XFunc<IEnumerable<String> 的转换,因此该重载是一个适用的候选者。

显然,出于同样的原因,另一个重载也是适用的候选者。

第三步:我们现在有两个适用的候选者。哪一个更“好”?
更“好”的是那个具有更具体类型的候选者。如果您有两个适用的候选者,M(Animal)M(Giraffe),我们选择Giraffe版本,因为Giraffe比Animal更具体。我们知道Giraffe更具体,因为每只Giraffe都是Animal,但并非每个Animal都是Giraffe。
但在您的情况下,没有一个类型比另一个更具体。两种Func类型之间没有转换。
因此,两者都不是更好的选择,因此重载解析报告错误。
然后,C# 4编译器似乎存在一个错误,其中其错误恢复模式仍会选择其中一个候选项,并报告另一个错误。我不清楚这是为什么。基本上,它表明错误恢复正在选择IEnumerable重载,然后指出方法组转换产生了一个不能接受的结果;即,字符串与IEnumerable<String>不兼容。
整个情况相当不幸。最好说如果返回类型不匹配,则没有方法组到委托转换。 (或者,产生错误的转换始终比未产生错误的转换更糟糕。)但是,我们现在被卡住了。
有趣的事实:Lambda的转换规则确实考虑了返回类型。如果您说Foo(()=>X()),那么我们做正确的事情。Lambda和方法组具有不同的可转换性规则,这个事实相当不幸。
总结一下,在这种情况下,编译器实际上是规范的正确实现,而这种特定情况则是某些可能不太幸运的规范选择的意外后果。

"如果返回类型不匹配,可能更好地说没有方法组到委托的转换。实际上,我认为改变这个规则不会造成破坏性的变化,你觉得呢?就我所知,似乎没有办法生成使用这种转换的可工作代码(也许我没有尝试足够努力,但我总是得到“XXX 具有错误的返回类型”)。" - Thomas Levesque
4
@Thomas:将曾经的错误转化为成功案例,可能会将过载解析问题从只有一种解决方案变为两种模糊的解决方案,这样就会将成功案例变成错误案例!令人惊讶的是,有多少事情在技术上算是破坏性变更,但在这种情况下我认为这样做可能是值得的。我会与 Mads 讨论这个问题。 - Eric Lippert
1
“重载决策总是忽略返回类型”,“lambda表达式和方法组有不同的可转换规则”——这很不幸,但我认为这是一个精彩的解释! - Jon Watte

7
您的代码需要进行两次“魔法”操作,一次将命名方法组转换为委托,另一次执行重载决策。
尽管您只有一个名为X的方法,但编译器规则是针对存在多个方法的情况而建立的。
此外,由于委托不必完全匹配方法签名,因此复杂度进一步增加。更重要的是,任何给定方法都可以转换为无数个具有相同签名的不同委托类型。
您的特定情况看起来很简单,但一般情况非常困难,因此语言不允许这样做。
如果您手动完成部分工作,就可以解决问题。例如:
Func<string> d = X;
Foo(d);

应该可以很好地编译。


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