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

时间:2016-10-25 21:07:01

标签: c# lambda overload-resolution

我的问题是由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&lt; Program.A&gt;)'和'Program.Call(Action&lt; Program.B&gt;)'

编译器无法推断xy的类型。可能x属于A类型而y类型为B,反之亦然,由于完全对称,两者都不是首选。到现在为止还挺好。现在,为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 => Call(y => M(x, y))的{​​{1}}和类型为x的{​​{1}}编译lambda表达式A。前者成功,而后者由于在尝试推断x的类型时检测到歧义而失败。因此,编译器得出结论:B必须是y类型。

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

  

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

目前的行为有充分的理由吗?是否只是让编译器的生活更轻松?或者它是编译器/规范中的缺陷?

1 个答案:

答案 0 :(得分:12)

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

在你的第一个场景中,唯一的可能性是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。

  

这很奇怪。

是的。在没有泛型类型推断和lambdas的世界中,重载分辨率已经足够了。对他们来说真的很难。

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

  • x是A,y是A失败
  • x是A,y是B作品
  • x是B,y是A作品
  • x是B,y是B作品
  • 因此有三种解决方案,没有一种更好,因此这是不明确的。

这不是它的工作原理。相反,我们将每个可能的类型分配提供给最外层的 lambda,并尝试推断每个的成功或失败。

如果你认为它有点奇怪,那么&#34;秩序很重要&#34; - 在某种意义上,外面的lambda&#34;特权&#34;在内部的lambdas上,当然,我可以看到那个论点。这是回溯算法的本质。

  

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

那仍然是真的。在你的第一个和第三个场景中,有一个解决方案可以推导出而没有矛盾;在第二个解决方案中有两个解决方案,这是不明确的。

  

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

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

  

只是让编译器的生活更轻松吗?

哈哈哈哈哈哈哈哈哈。

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

  

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

没有。

规范过程的目标是提出一种设计,根据我们在LINQ标准库中看到的各种过载,产生合理的推论。我认为我们实现了这一目标。 &#34;添加过载永远不会导致模糊程序变得非模糊&#34;在任何时候都不是规范过程的目标。