为什么此多态C#代码打印出它的功能?


69

最近,为我提供了以下代码,作为一种难题,以帮助理解OOP-C#中的多态性继承

// No compiling!
public class A
{
     public virtual string GetName()
     {
          return "A";
     }
 }

 public class B:A
 {
     public override string GetName()
     {
         return "B";
     }
 }

 public class C:B
 {
     public new string GetName()
     {
         return "C";
     }
 }

 void Main()
 {
     A instance = new C();
     Console.WriteLine(instance.GetName());
 }
 // No compiling!

现在,在与提出该难题的其他开发人员进行了长时间的聊天之后,我知道了输出的内容,但是我不会为您带来麻烦。我真正遇到的唯一问题是我们如何获得该输出,代码如何逐步执行,继承什么内容等等。

我以为C会返回,因为这似乎是已定义的类。然后我想知道是否B会因为C继承而返回B-但B也继承了A(这让我感到困惑!)。


题:

谁能解释多态性和继承性在检索最终显示在屏幕上的输出中如何发挥作用?


4
哇!我以为我知道继承/多态性。乍一看,我确实忽略了virtual + override + new,但是当我看到它们时,我真的很困惑。+1是个好问题!
罗伯特·科里特尼克

Answers:


99

考虑这一点的正确方法是,想象每个类都要求其对象具有一定数量的“插槽”。这些插槽充满了方法。问题“实际上调用了什么方法?” 需要您弄清楚两件事:

  1. 每个插槽的内容是什么?
  2. 哪个插槽叫?

让我们从考虑插槽开始。有两个插槽。A的所有实例都必须具有一个插槽,我们将其称为GetNameSlotA。C的所有实例都必须具有一个插槽,我们将其称为GetNameSlotC。这就是C中声明中“ new”的含义-意思是“我想要一个新插槽”。与B中声明上的“覆盖”相比,这意味着“我不需要新的插槽,我想重新使用GetNameSlotA”。

当然,C继承自A,因此C还必须具有插槽GetNameSlotA。因此,C的实例具有两个插槽-GetNameSlotA和GetNameSlotC。非C的A或B实例具有一个插槽GetNameSlotA。

现在,当您创建一个新的C时,这两个插槽中有什么内容?共有三种方法,分别称为GetNameA,GetNameB和GetNameC。

A的声明为“将GetNameA放入GetNameSlotA”。A是C的超类,因此A的规则适用于C。

B的声明为“将GetNameB放入GetNameSlotA”。B是C的超类,因此B的规则适用于C的实例。现在我们在A和B之间存在冲突。B是派生性更高的类型,因此它胜出了-B的规则覆盖了A的规则。因此,声明中的“替代”一词。

C的声明为“将GetNameC放入GetNameSlotC”。

因此,您的新C将有两个插槽。GetNameSlotA将包含GetNameB,而GetNameSlotC将包含GetNameC。

现在,我们确定了哪些方法位于哪个插槽中,因此我们已经回答了第一个问题。

现在我们必须回答第二个问题。叫什么插槽?

就像您是编译器一样考虑一下。您有一个变量。您所知道的是它的类型为A。系统要求您解决对该变量的方法调用。您查看A上可用的插槽,唯一可以找到匹配的插槽是GetNameSlotA。您不了解GetNameSlotC,因为您只有一个类型A的变量。您为什么要寻找仅适用于C的插槽?

因此,这是对GetNameSlotA中任何内容的调用。我们已经确定在运行时,GetNameB将位于该插槽中。因此,这是对GetNameB的调用。

这里的关键要点是,在C#中,重载解析会选择一个插槽并生成对该插槽中任何内容的调用。


1
极好的答案。还有其他一些表现很好的方法,但是这一方法确实可以解释当前的问题。好解释。
丹尼尔(Daniel)2009年5

1
我知道这是一个古老的功课,只是想在VS“观察”名单如何,如果能看到(或其他地方),该实例是A型的,因为它显示了它作为C型上instanct.GetType()
BlueChippy

1
@BlueChippy:对象知道如何用C#描述自己的类型;显然,这就是GetType所做的。变量知道如何向调试器描述自己的类型。因此,调试器既知道变量的类型,又知道该变量中对象的类型(可能更具体)。这样就可以向你们展示。
埃里克·利珀特

1
@EricLippert:谢谢埃里克,但我怎么看?如果我将一个手表添加到实例,它将显示为C类型-错误地导致认为将调用“新”方法。但是,如果实例是类型A的var-那就是实际所调用的。但是,当调试或调用GetType()时,VS只显示C型...想知道我如何将实例作为“ A型”存储在“ C型”实例中?
BlueChippy

从该答案中意识到“不编译!”真是有趣。从问题陈述实际上是一个提示,编译器(与运行时相比)足以预测输出。
groch

24

它应返回“ B”,因为B.GetName()A.GetName()函数保存在该函数的小虚拟表框中。C.GetName()是编译时的“替代”,它不会替代虚拟表,因此您无法通过指向的指针来检索它A


说得好,而且肯定比我输入的答案短很多。
布赖恩·拉斯穆森

10
我认为使用短语“编译时'替代'”C.GetName()有点误导,因为它没有替代任何内容。几乎所有其他单词都将更有用:)
乔恩·斯凯特

给定一个从c继承并再次覆盖GetName的类D ...给定在main中实例化的类是D,结果将是什么?
蟾蜍

我只是称其为“编译时替换”。
Joren

5
只有一个vtable,但是一个new方法只是碰巧具有相同名称的另一种方法。编译器根据接收者的静态类型选择要使用的vtable插槽。((C)foo).GetName()将始终调用C.GetName或对其进行覆盖,C.GetName如果它是虚拟的,((A)foo).GetName()则将始终调用A.GetName或其覆盖之一。这里C.GetName及其替代不是的替代A.GetName,它们是不同的方法。
Joren

3

容易,您只需要记住继承树即可。

在您的代码中,您拥有对类型“ A”的类的引用,该类由类型“ C”的实例实例化。现在,要为虚拟的“ GetName()”方法解析确切的方法地址,编译器将沿继承层次结构向上移动并查找最新的覆盖(请注意,只有“虚拟”是覆盖,“新”是完全不同的东西) ...)。

简而言之,发生了什么。如果您要在类型“ C”的实例上调用它,则类型“ C”中的new关键字将仅起作用,然后编译器将完全否定所有可能的继承关系。严格地说,这与多态性完全没有关系-从事实可以看出,使用'new'关键字屏蔽虚拟方法还是非虚拟方法都没有任何区别...

类'C'中的'新建'的确切含义是:如果您在此(精确)类型的实例上调用'GetName()',则将所有内容忘掉并使用THIS方法。相反,“虚拟”的意思是:不管调用实例的确切类型是什么,都应沿继承树查找具有该名称的方法。


“寻找最近的替代品”,那么您将如何描述此印刷品C
Anirban Nag'tintinmj'14

2

好的,帖子有点陈旧,但这是一个很好的问题,也是一个很好的答案,所以我只想补充我的想法。

考虑以下示例,该示例与之前的示例相同,但主要功能不同:

// No compiling!
public class A
{
    public virtual string GetName()
    {
        return "A";
    }
}

public class B:A
{
    public override string GetName()
    {
        return "B";
    }
}

public class C:B
{
    public new string GetName()
    {
        return "C";
    }
}

void Main()
{
    Console.Write ( "Type a or c: " );
    string input = Console.ReadLine();

    A instance = null;
    if      ( input == "a" )   instance = new A();
    else if ( input == "c" )   instance = new C();

   Console.WriteLine( instance.GetName() );
}
// No compiling!

现在很明显,函数调用不能在编译时绑定到特定函数。但是,必须进行一些编译,并且该信息只能取决于引用的类型。因此,将不可能使用C类型之一以外的任何引用来执行C类的GetName函数。

PS也许我应该用术语方法代替函数,但是正如莎士比亚所说:一个具有其他名称的函数仍然是一个函数:)


-1

实际上,我认为它应该显示C,因为new运算符只是隐藏了所有具有相同名称的祖先方法。因此,在隐藏A和B方法的情况下,只有C保持可见。

http://msdn.microsoft.com/zh-cn/library/51y09td4%28VS.71%29.aspx#vclrfnew_newmodifier


1
是的,但这仍然是编译时的事情。如果仅提供对的引用A,则编译器将不知道您的方法可能是隐藏的,它所拥有的只是其可使用的虚拟表,并且唯一可以覆盖虚拟表插槽的是override关键字。
布林迪

2
现在,如果将A指针下移到C,您将获得不同的结果。
布林迪

是的,这真的很有趣。
2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.