【问题标题】:Ambiguity in parameter type inference for C# lambda expressionsC# lambda 表达式的参数类型推断中的歧义
【发布时间】:2016-10-25 21:07:00
【问题描述】:

我的问题是由 Eric Lippert 的 this blog post 引起的。考虑以下代码:

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))); }
}

这编译成功并打印M(A, B),因为编译器计算出lambda表达式中xy的类型应该分别是AB。现在,为Program.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.Call(Action)”

编译器无法推断xy 的类型。 x 可能是 A 类型,yB 类型,反之亦然,由于完全对称,两者都不是首选。到现在为止还挺好。现在,为Program.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))); }
}

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

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

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

当前的行为有什么好的理由吗?仅仅是让编译器的生活更轻松的问题吗?还是编译器/规范中的缺陷?

【问题讨论】:

  • 原因是方法的返回类型不是其签名的一部分。因此,在解决正确的重载时,编译器只查看方法的参数。
  • @viveknuna 所有方法都返回void,返回类型与什么有什么关系?

标签: c# lambda overload-resolution


【解决方案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 具有“特权”,那么,当然,我可以看到这个论点。这就是回溯算法的本质。

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

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

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

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

仅仅是为了让编译器的工作更轻松吗?

哈哈哈哈哈哈哈哈哈

我花了一年的大部分时间来设计、指定、实施和测试所有这些逻辑,我的许多同事也花了很多时间和精力。 Easy 没有涉及它的任何部分。

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

没有。

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

【讨论】:

  • 我了解规则。但我认为重载决议的目的是编译器试图对程序员的意图做出最好的猜测,检测和报告歧义,而不是做出任意选择。因此,当假设 x 为 B 解析内部 lambda 表达式时,编译器可能会以不同于其他错误的方式处理歧义。我的意思是,编译器不会认为“假设 x 是 B 会产生错误”,而是会认为“假设 x 是 B 会产生歧义;我正在寻找歧义,而我刚刚找到了它”。但也许这要求太高了。
  • 不过,我认为“如果它有多个解决方案,则编译失败并出现歧义错误”这句话。从您的博客文章中引用了 SAT 公式,这是不正确的,或者至少是误导性的。如果将 Main 函数的主体更改为 M(a =&gt; M(b =&gt; M(c =&gt; MustBeT(And(Or(a, b, c), Not(c))))));,则代码编译成功,尽管公式 (a | b | c) &amp; (!c) 有多个解决方案。
  • @Bartosz:这是一个很好的批评;你说得对,这篇文章以这种方式具有误导性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-01
  • 2023-04-05
  • 2020-11-05
  • 2023-03-20
相关资源
最近更新 更多