提问人:Bartosz 提问时间:10/26/2016 更新时间:10/26/2016 访问量:605
C# lambda 表达式的参数类型推断中的歧义
Ambiguity in parameter type inference for C# lambda expressions
问:
我的问题是由埃里克·利珀特(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)
x
y
A
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)"); } // 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>)”
编译器无法推断 和 的类型。它可能是类型,也可能是类型,反之亦然,由于完全对称,两者都不能优先。目前为止,一切都好。现在,再添加一个重载:x
y
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))); }
}
这将成功编译并再次打印!我能猜到原因。编译器解决了尝试编译 of type 和 for of type 的 lambda 表达式的重载问题。前者成功,而后者失败,因为在尝试推断 的类型时检测到歧义。因此,编译器得出结论,必须是 类型的 。M(A, B)
Program.Call
x => Call(y => M(x, y))
x
A
x
B
y
x
A
因此,增加更多的歧义会导致更少的歧义。这很奇怪。而且,这与埃里克在上述帖子中所写的不一致:
如果它有多个解决方案,则编译失败并出现歧义错误。
目前的行为有什么充分的理由吗?这仅仅是让编译器的生活更轻松的问题吗?或者它是编译器/规范中的缺陷?
答:
有趣的场景。让我们考虑一下编译器如何分析每个。
在第一种情况下,唯一的可能性是 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 标准库中看到的各种重载来产生合理的推论。我认为我们实现了这个目标。“添加重载永远不会导致模棱两可的程序变得不模棱两可”在任何时候都不是规范过程的目标。
评论
M(a => M(b => M(c => MustBeT(And(Or(a, b, c), Not(c))))));
(a | b | c) & (!c)
评论
void