我们认为此示例显示 C# 编译器中的错误。
让我们在出现编译器错误时做我们应该做的事情:仔细对比预期和观察到的行为。
观察到的行为是程序分别产生 11 和 101 作为第一和第二输出。
预期的行为是什么?有两个“虚拟插槽”。第一个输出应该是调用Foo(T)
槽中的方法的结果。第二个输出应该是调用Foo(S)
槽中的方法的结果。
这些插槽中有什么?
在方法的实例中Base<T,S>
进入槽,方法进入槽。return 1
Foo(T)
return 2
Foo(S)
在一个实例中Intermediate<T,S>
,return 11
方法进入Foo(T)
槽,return 2
方法进入Foo(S)
槽。
希望到目前为止你同意我的看法。
在 的实例中Conflict
,有四种可能性:
- 可能性一:
return 11
方法入Foo(T)
槽,return 101
方法入Foo(S)
槽。
- 可能性二:
return 101
方法入Foo(T)
槽,return 2
方法入Foo(S)
槽。
- 可能性三:该
return 101
方法适用于两个插槽。
- 可能性四:编译器检测到程序有歧义,报错。
根据规范的第 10.6.4 节,您希望这里会发生两件事中的一件。任何一个:
- 编译器会判断 in 的方法
Conflict
覆盖了 in 的方法Intermediate<string, string>
,因为中间类中的方法是先找到的。在这种情况下,可能性二是正确的行为。或者:
- 编译器将确定 in 中的方法对于它覆盖的原始
Conflict
声明是模棱两可的,因此可能性四是正确的。
在这两种情况下,可能性都不是正确的。
我承认,这不是 100% 清楚,这两者中哪一个是正确的。我个人的感觉是,更明智的行为是将覆盖方法视为中间类的私有实现细节;我想到的相关问题不是中间类是否覆盖基类方法,而是它是否声明了具有匹配签名的方法。在这种情况下,正确的行为是选择可能性四。
编译器实际所做的是您所期望的:它选择了可能性二。因为中间类有一个匹配的成员,所以我们选择它作为“要覆盖的东西”,而不管中间类中没有声明该方法的事实。编译器确定这Intermediate<string, string>.Foo
是被 覆盖的方法Conflict.Foo
,并相应地发出代码。它不会产生错误,因为它判断程序没有错误。
因此,如果编译器正确地分析了代码,选择了可能性二并且没有产生错误,那么为什么在运行时编译器似乎选择了可能性一,而不是可能性二?
因为在泛型构造下制作一个导致两种方法统一的程序是运行时的实现定义的行为。在这种情况下,运行时可以选择做任何事情!它可以选择给出类型加载错误。它可能会产生可验证性错误。它可以选择允许程序,但根据自己选择的某些标准填充插槽。事实上,后者就是它所做的。运行时会查看 C# 编译器发出的程序,并自行决定分析该程序的正确方法。
所以,现在我们有一个相当哲学的问题,这是否是编译器错误;编译器遵循规范的合理解释,但我们仍然没有得到我们期望的行为。从这个意义上说,它在很大程度上是一个编译器错误。编译器的工作是将用 C# 编写的程序翻译成用 IL 编写的完全等效的程序。编译器没有这样做;它将用 C# 编写的程序转换为用 IL 编写的程序,该程序具有实现定义的行为,而不是 C# 语言规范指定的行为。
正如 Sam 在他的博客文章中清楚地描述的那样,我们很清楚 C# 语言赋予哪些类型拓扑具有特定含义与 CLR 赋予哪些拓扑具有特定含义之间的这种不匹配。C# 语言相当清楚,可能性 2 可以说是正确的一种,但是没有任何代码可以让 CLR 做到这一点,因为CLR 从根本上具有实现定义的行为,只要两个方法统一以具有相同的签名。因此,我们的选择是:
- 没做什么。允许这些疯狂的、不切实际的程序继续具有与 C# 规范不完全匹配的行为。
- 使用启发式。正如 Sam 所指出的,我们可以更聪明地使用元数据机制来告诉 CLR 哪些方法会覆盖哪些其他方法。但是……这些机制使用方法签名来消除歧义,现在我们又回到了以前的状态;我们现在使用具有实现定义行为的机制来消除具有实现定义行为的程序的歧义!这是一个非首发。
- 导致编译器在可能发出其行为由运行时实现定义的程序时产生警告或错误。
- 修复 CLR,以使导致方法在签名中统一的类型拓扑的行为得到明确定义并与 C# 语言的行为相匹配。
最后一个选择非常昂贵。支付这笔费用给我们带来了微乎其微的用户利益,并直接从解决用户编写明智程序所面临的实际问题中抽走了预算。无论如何,这样做的决定完全不在我的掌控之中。
因此,我们 C# 编译器团队选择了第一种和第三种策略的组合;有时我们会为这种情况产生警告或错误,有时我们什么也不做,让程序在运行时做一些奇怪的事情。
由于在实践中这类程序很少出现在现实的业务线编程场景中,所以我对这些极端情况并不感到很糟糕。如果它们便宜且易于修复,那么我们会修复它们,但它们既不便宜也不易于修复。
如果您对此主题感兴趣,请参阅我的文章,了解另一种导致两种方法统一导致警告和实现定义的行为的方式:
http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx
http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx