C# lambda 表达式的参数类型推断中的歧义

Ambiguity in parameter type inference for C# lambda expressions

提问人:Bartosz 提问时间:10/26/2016 更新时间:10/26/2016 访问量:605

问:

我的问题是由埃里克·利珀特(Eric Lippert)的这篇博文引发的。请考虑以下代码:

using System;
class Program {
    class A {}
    class B {}
    static void M(A x, B y) { Console.WriteLine("M(A, B)"); }
    static void Call(Action<A> f) { f(new A()); }
    static void Call(Action<B> f) { f(new B()); }
    static void Main() { Call(x => Call(y => M(x, y))); }
}

这成功编译并打印,因为编译器确定 lambda 表达式中 和 的类型应该分别是 和。现在,为 : 添加重载:M(A, B)xyABProgram.M

using System;
class Program {
    class A {}
    class B {}
    static void M(A x, B y) { Console.WriteLine("M(A, B)"); }
    static void M(B x, A y) { Console.WriteLine("M(B, A)"); } // added line
    static void Call(Action<A> f) { f(new A()); }
    static void Call(Action<B> f) { f(new B()); }
    static void Main() { Call(x => Call(y => M(x, y))); }
}

这会产生编译时错误:

错误 CS0121:以下方法或属性之间的调用不明确:“Program.Call(Action<Program.A>)”和“Program.Call(Action<Program.B>)”

编译器无法推断 和 的类型。它可能是类型,也可能是类型,反之亦然,由于完全对称,两者都不能优先。目前为止,一切都好。现在,再添加一个重载:xyxAyBProgram.M

using System;
class Program {
    class A {}
    class B {}
    static void M(A x, B y) { Console.WriteLine("M(A, B)"); }
    static void M(B x, A y) { Console.WriteLine("M(B, A)"); }
    static void M(B x, B y) { Console.WriteLine("M(B, B)"); } // added line
    static void Call(Action<A> f) { f(new A()); }
    static void Call(Action<B> f) { f(new B()); }
    static void Main() { Call(x => Call(y => M(x, y))); }
}

这将成功编译并再次打印!我能猜到原因。编译器解决了尝试编译 of type 和 for of type 的 lambda 表达式的重载问题。前者成功,而后者失败,因为在尝试推断 的类型时检测到歧义。因此,编译器得出结论,必须是 类型的 。M(A, B)Program.Callx => Call(y => M(x, y))xAxByxA

因此,增加更多的歧义会导致更少的歧义。这很奇怪。而且,这与埃里克在上述帖子中所写的不一致:

如果它有多个解决方案,则编译失败并出现歧义错误。

目前的行为有什么充分的理由吗?这仅仅是让编译器的生活更轻松的问题吗?或者它是编译器/规范中的缺陷?

C# Lambda 重载解决方法

评论

0赞 Vivek Nuna 10/26/2016
原因是方法的返回类型不是其签名的一部分。因此,在解析正确的重载时,编译器只查看方法的参数。
2赞 Kyle 10/26/2016
@viveknuna所有方法的返回,返回类型与任何事情有什么关系?void

答:

12赞 Eric Lippert 10/26/2016 #1

有趣的场景。让我们考虑一下编译器如何分析每个。

在第一种情况下,唯一的可能性是 x 是 A,y 是 B。

在第二种情况下,我们可以让 x 是 A,y 是 B,或者 x 是 B,y 是 A。

现在考虑您的第三种情况。让我们从假设 x 是 B 开始。如果 x 是 B,那么 y 可以是 A 或 B。我们没有理由选择 A 或 B 来表示 y。因此,x 为 B 的程序是模棱两可的。因此 x 不能是 B;我们的推测一定是错的。

因此,要么 x 是 A,要么程序是错误的。x 可以是 A 吗?如果是,则 y 必须是 B。如果 x 是 A,我们推导出没有错误,如果 x 是 B,我们推导出一个错误,所以 x 必须是 A。

由此我们可以推断出 x 是 A,y 是 B。

这很奇怪。

是的。在没有泛型类型推断和 lambda 的世界中,重载解决已经够难了。有了他们,这真的非常困难。

我怀疑你的困难在于对第三种情况的分析似乎更好:

  • x 是 A,y 是 A 失败
  • x 是 A,y 是 B 工作
  • x 是 B,y 是 A 作品
  • x 是 B,y 是 B 工作
  • 因此有三种解决方案,没有一种更好,因此这是模棱两可的。

但事实并非如此。相反,我们将所有可能的类型赋值都分配给最外层的 lambda,并尝试推断每个类型的成功或失败。

如果你认为“秩序很重要”有点奇怪——从某种意义上说,外部的 lambda 比内部的 lambda “享有特权”,那么,当然,我可以看到这个论点。这就是回溯算法的本质。

如果它有多个解决方案,则编译失败并出现歧义错误。

这仍然是正确的。在你的第一个和第三个场景中,有一个解决方案可以毫无矛盾地推导出来;在第二种情况下,有两种解决方案,这是模棱两可的。

目前的行为有什么充分的理由吗?

是的。我们非常仔细地考虑了这些规则。像所有设计决策一样,有一个妥协的过程。

这仅仅是让编译器的生活更轻松的问题吗?

哈哈哈哈哈

我花了大半年的时间来设计、指定、实现和测试所有这些逻辑,我的许多同事也花费了大量的时间和精力。简单不会进入它的任何部分。

或者它是编译器/规范中的缺陷?

不。

规范过程的目标是提出一种设计,该设计能够根据我们在 LINQ 标准库中看到的各种重载来产生合理的推论。我认为我们实现了这个目标。“添加重载永远不会导致模棱两可的程序变得不模棱两可”在任何时候都不是规范过程的目标。

评论

0赞 Bartosz 10/26/2016
我明白规则。但我认为重载解决的目的是编译器试图对程序员的意图做出最佳猜测,检测和报告歧义,而不是做出任意选择。因此,在解析内部 lambda 表达式时,假设 x 为 B,编译器可以采用与其他错误不同的方式处理歧义。我的意思是,编译器可以认为“假设 x 是 B 会产生歧义”,而不是认为“假设 x 是 B 会产生歧义;我正在寻找一种模棱两可的地方,而我刚刚找到了它”。但也许这要求太多了。
2赞 Bartosz 10/26/2016
尽管如此,我认为您的博客文章中提到的“如果它有多个解决方案,则编译失败并出现歧义错误”这句话是不正确的,或者至少具有误导性。如果将 Main 函数的主体更改为 ,则代码编译成功,尽管公式具有多个解决方案。M(a => M(b => M(c => MustBeT(And(Or(a, b, c), Not(c))))));(a | b | c) & (!c)
1赞 Eric Lippert 10/26/2016
@Bartosz:这是一个很好的批评;你是对的,这篇文章以这种方式具有误导性。