23

考虑以下代码(它有点长,但希望你能遵循):

class A
{
}

class B : A
{
}

class C
{
    public virtual void Foo(B b)
    {
        Console.WriteLine("base.Foo(B)");
    }
}

class D: C
{
    public override void Foo(B b)
    {
        Console.WriteLine("Foo(B)");
    }

    public void Foo(A a)
    {
        Console.WriteLine("Foo(A)");
    }
}

class Program
{
    public static void Main()
    {
        B b = new B();
        D d = new D ();
        d.Foo(b);
    }
}

如果您认为该程序的输出是“Foo(B)”,那么您将和我在同一条船上:完全错误!事实上,它输出“Foo(A)”

如果我从类中删除虚拟方法C,那么它会按预期工作:“Foo(B)”是输出。

为什么编译器会选择采用AwhenB是更多派生类的版本?

4

5 回答 5

15

答案在 C# 规范第 7.3节和第 7.5.5.1 节中

我分解了用于选择调用方法的步骤。

  • 首先,构造N=Foo在 T ( ) 中声明的所有名为 N ( ) 的可访问成员和 T ( T=class D) 的基类型的集合class C包含覆盖修饰符的声明被排除在集合之外D.Foo(B) 是 exclude

    S = { C.Foo(B) ; D.Foo(A) }
    
  • 构造方法调用的候选方法集。从先前成员查找找到的与 M 关联的方法集开始,该集被简化为适用于参数列表 AL ( AL=B) 的那些方法。集合缩减包括对集合中的每个方法 TN 应用以下规则,其中 T ( ) 是声明T=class D方法 N ( ) 的类型N=Foo

    • 如果 N 不适用于 AL(第 7.4.2.1 节),则从集合中删除 N。

      • C.Foo(B)适用于 AL
      • D.Foo(A)适用于 AL

        S = { C.Foo(B) ; D.Foo(A) }
        
    • 如果 N 适用于 AL(第 7.4.2.1 节),则从 set 中删除所有在 T 基类型中声明的方法C.Foo(B)从集合中移除

          S = { D.Foo(A) }
      

最后获胜者是D.Foo(A)


如果抽象方法从 C 中删除

如果从 C 中删除抽象方法,则初始集合是S = { D.Foo(B) ; D.Foo(A) },并且必须使用重载决策规则来选择该集合中的最佳函数成员

在这种情况下,获胜者是D.Foo(B)

于 2010-09-09T07:11:02.953 回答
10

当 B 是更多派生类时,为什么编译器会选择采用 A 的版本?

正如其他人所指出的,编译器这样做是因为这是语言规范所说的。

这可能是一个不满意的答案。一个自然的后续是“决定以这种方式指定语言的设计原则是什么?”

这是 StackOverflow 和我的邮箱中的一个常见问题。简短的回答是“这种设计减轻了 Brittle Base Class 系列的错误。”

有关该功能的描述及其设计方式的原因,请参阅我关于该主题的文章:

http://blogs.msdn.com/b/ericlippert/archive/2007/09/04/future-break-changes-part-three.aspx

有关各种语言如何处理脆性基类问题的更多文章,请参阅我关于该主题的文章存档:

http://blogs.msdn.com/b/ericlippert/archive/tags/brittle+base+classes/

这是我上周对同一个问题的回答,看起来非常像这个问题。

为什么忽略基类中声明的签名?

以下是三个更相关或重复的问题:

C#重载决议?

方法重载决议和 Jon Skeet 的脑筋急转弯

为什么这行得通?方法重载+方法覆盖+多态

于 2010-09-09T14:57:05.553 回答
2

我认为这是因为在非虚拟方法的情况下,使用了调用该方法的变量的编译时类型。

您有非虚拟的 Foo 方法,因此该方法被调用。

这个链接有很好的解释http://msdn.microsoft.com/en-us/library/aa645767%28VS.71%29.aspx

于 2010-09-09T07:02:47.420 回答
2

所以,这里是它应该如何根据规范工作(在编译时,并且考虑到我正确地浏览了文档):

D编译器根据方法名称和参数列表从类型及其基类型中识别出匹配方法的列表。这意味着任何名为 的方法Foo,采用具有隐式转换的类型的一个参数B都是有效的候选者。这将产生以下列表:

C.Foo(B) (public virtual)
D.Foo(B) (public override)
D.Foo(A) (public)

从这个列表中,任何包含覆盖修饰符的声明都被排除在外。这意味着该列表现在包含以下方法:

C.Foo(B) (public virtual)
D.Foo(A) (public)

此时我们有了匹配候选者的列表,编译器现在决定调用什么。在文档7.5.5.1 Method invocations中,我们找到以下文本:

如果 N 适用于 A(第 7.4.2.1 节),则从集合中删除在 T 的基类型中声明的所有方法。

这实质上意味着如果在 中声明了适用的方法,则D基类中的任何方法都将从列表中删除。在这一点上,我们有一个赢家:

D.Foo(A) (public)
于 2010-09-09T07:36:24.767 回答
0

我认为,在实现另一个类时,它会在树的最上方查找一个方法的可靠实现。由于没有调用方法,因此它使用的是基类。

public void Foo(A a){
    Console.WriteLine("Foo(A)" + a.GetType().Name);
    Console.WriteLine("Foo(A)" +a.GetType().BaseType );
}

那是猜测我不是.Net的专业人士

于 2010-09-09T07:11:48.290 回答