我们相信这个例子在C#编译器中出现了一个错误(如果我们错了,请取笑我)。这个错误可能是众所周知的:毕竟,我们的例子是对in this blog post所描述内容的简单修改。
using System;
namespace GenericConflict
{
class Base<T, S>
{
public virtual int Foo(T t)
{ return 1; }
public virtual int Foo(S s)
{ return 2; }
public int CallFooOfT(T t)
{ return Foo(t); }
public int CallFooOfS(S s)
{ return Foo(s); }
}
class Intermediate<T, S> : Base<T, S>
{
public override int Foo(T t)
{ return 11; }
}
class Conflict : Intermediate<string, string>
{
public override int Foo(string t)
{ return 101; }
}
static class Program
{
static void Main()
{
var conflict = new Conflict();
Console.WriteLine(conflict.CallFooOfT("Hello mum"));
Console.WriteLine(conflict.CallFooOfS("Hello mum"));
}
}
}
这个想法只是创建一个具有两个虚拟方法的类Base<T, S>
,在T
和S
的“邪恶”选择之后,这些方法的签名将变得相同。类Conflict
只重载其中一个虚方法,并且由于Intermediate<,>
的存在,它应该被明确定义哪一个!
但是当程序运行时,输出似乎表明错误的重载被覆盖了。
当我们阅读Sam Ng的follow-up post时,我们得到的结论是该错误没有得到修复,因为他们认为总会抛出类型加载异常。但在我们的示例中,代码编译并运行时没有错误(只是意外的输出)。
答案 0 :(得分:21)
我们相信这个例子在C#编译器中出现了一个错误。
在展示编译器错误时,让我们做我们应该做的事情:仔细对比预期和观察到的行为。
观察到的行为是程序分别产生11和101作为第一和第二输出。
预期的行为是什么?有两个&#34;虚拟插槽&#34;。第一个输出应该是在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节,您希望这里发生两件事之一。之一:
Conflict
中的方法会覆盖Intermediate<string, string>
中的方法,因为首先找到中间类中的方法。在这种情况下,可能性2是正确的行为。或者:Conflict
中的方法对于它覆盖的原始声明是不明确的,因此可能性4是正确的。 在任何情况下都不可能是正确的。
我承认,这两个中的哪一个是正确的,这不是100%明确的。我个人的感觉是,更明智的行为是将覆盖方法视为中间类的私有实现细节;我想到的相关问题不是中间类是否覆盖基类方法,而是是否声明具有匹配签名的方法。在这种情况下,正确的行为是选择可能性四。
编译器的实际功能是你所期望的:它选择了两种可能性。因为中间类有一个匹配的成员,所以我们选择它作为&#34;覆盖&#34;的东西,不管该方法在中间类中是否声明。编译器确定Intermediate<string, string>.Foo
是由Conflict.Foo
覆盖的方法,并相应地发出代码。它不会产生错误,因为它判断程序没有错误。
因此,如果编译器正确分析代码,选择可能性2,而不是产生错误,那么为什么在运行时 出现 编译器选择了可能性,不是两种可能性?
因为在一般构造下创建一个导致两个方法统一的程序是运行时的实现定义行为。在这种情况下,运行时可以选择任何!它可以选择给出类型加载错误。它可以提供可验证性错误。它可以选择允许程序,但根据自己选择的一些标准填写插槽。事实上,后者就是它的作用。运行时会查看C#编译器发出的程序,并自行决定分析此程序的正确方法。
所以,现在我们有一个相当哲学的问题,即这是否是编译器错误;编译器遵循对规范的合理解释,但我们仍然没有得到我们期望的行为。从这个意义上讲,它很大程度上是编译错误。 编译器的工作是将用C#编写的程序转换为用IL编写的完全等效的程序。编译器未能这样做;它正在将用C#编写的程序转换为用IL编写的程序,该程序具有实现定义的行为,而不是C#语言规范指定的行为。
正如Sam在他的博客文章中清楚地描述的那样,我们清楚地意识到C#语言赋予特定含义的类型拓扑与CLR赋予特定含义的拓扑之间的这种不匹配。 C#语言相当清楚,可能性2可以说是正确的,但是没有我们可以发出的代码使得CLR做到了因为 CLR从根本上有任何时候实现定义的行为两种方法统一以具有相同的签名。因此,我们的选择是:最后的选择非常昂贵。支付这笔费用可以为我们带来极小的用户利益,并直接将预算从解决用户编写合理程序所面临的现实问题中解放出来。无论如何,做到这一点的决定完全不在我手中。
我们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