为啥这个多态 C# 代码会打印它的功能?

Posted

技术标签:

【中文标题】为啥这个多态 C# 代码会打印它的功能?【英文标题】:Why does this polymorphic C# code print what it does?为什么这个多态 C# 代码会打印它的功能? 【发布时间】:2010-12-03 06:16:33 【问题描述】:

我最近收到了以下代码,作为一个谜题,以帮助理解 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(这让我感到困惑!)。


问题:

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

【问题讨论】:

哇!我以为我知道继承/多态性......好吧。确实,乍一看,我忽略了 virtual+override+new,但是当我确实看到它们时,我真的很困惑。 +1 提出一个好问题! 【参考方案1】:

好的,帖子有点老了,但这是一个很好的问题和一个很好的答案,所以我只是想补充一下我的想法。

考虑下面的例子,除了主函数之外,它和之前一样:

// 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 函数。

附:也许我应该使用术语方法而不是函数,但正如莎士比亚所说:任何其他名称的函数仍然是函数:)

【讨论】:

【参考方案2】:

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

    每个槽的内容是什么? 调用哪个槽?

让我们从考虑插槽开始。有两个插槽。 A 的所有实例都需要有一个我们称之为 GetNameSlotA 的槽。 C 的所有实例都必须有一个我们称之为 GetNameSlotC 的槽。这就是 C 中声明中“新”的含义——它的意思是“我想要一个新插槽”。相比于 B 中声明的“override”,意思是“我不想要新的 slot,我要重用 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# 重载解析中选择一个槽并生成对该槽中的任何内容的调用。

【讨论】:

优秀的答案。还有一些其他的做得很好,但这个确实解释了手头的问题。解释得很好。 我知道这是一个旧作业,但只是想知道如何在 VS“监视”列表(或其他任何地方)中看到该实例属于 A 类型,因为它显示为 C 类型一个 instanct.GetType() @BlueChippy:对象知道如何在 C# 中描述自己的类型;显然这就是 GetType 所做的。变量知道如何向调试器描述自己的类型。所以调试器既知道变量的类型是什么,也知道变量中对象的可能更具体的类型。所以它可以同时显示给你。 @EricLippert:谢谢 Eric,但我怎么看出来的?如果我向实例添加一个手表,它会显示为 C 类型 - 导致错误地认为将调用“新”方法。但是,因为实例是 A 类型的 var - 这就是实际调用的内容。但是,在调试或调用 GetType() 时,VS 仅显示 C 类型...想知道我如何看到该实例是 C 类型“存储/持有”为 A 类型? 从那个答案中意识到“没有编译!”很有趣。从问题陈述实际上是一个提示,编译器(与运行时)足以预测输出。【参考方案3】:

很简单,您只需要记住继承树即可。

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

简而言之,就是这样。 'C' 类型的 new 关键字只有在你在'C' 类型的实例上调用它时才会起作用,然后编译器会完全否定所有可能的继承关系。严格来说,这与多态性完全没有关系——你可以从这样一个事实中看出,无论你用 'new' 关键字屏蔽虚拟方法还是非虚拟方法都没有任何区别......

'C' 类中的'New' 的确切含义是:如果您在此(确切)类型的实例上调用 'GetName()',则忘记所有内容并使用 THIS 方法。 'Virtual' 相反的意思是:无论调用实例的确切类型是什么,沿着继承树向上直到找到具有此名称的方法。

【讨论】:

“查找最近的覆盖”然后你会如何描述this printing C【参考方案4】:

其实我觉得应该显示C,因为new操作符只是隐藏了all同名的祖先方法。因此,隐藏了 A 和 B 的方法后,只有 C 保持可见。

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

【讨论】:

是的,但它仍然是编译时的事情。如果您所拥有的只是对A 的引用,那么编译器不知道您的方法可能被隐藏,它所拥有的只是要使用的虚拟表,而唯一覆盖虚拟表插槽的是@987654323 @关键字。 现在,如果您将A 指针向下投射回C,您将得到不同的结果。 是的,这真的很有趣。【参考方案5】:

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

【讨论】:

说得好,而且肯定比我输入的答案短很多。 我认为对C.GetName() 使用短语“编译时'覆盖'”有点误导,因为它没有覆盖任何东西。几乎任何其他词都会更有用:) 给定一个继承自 c 并再次覆盖 GetName 的类 D...如果在 main 中实例化的类的结果是 D? 我称之为“编译时替换”。 只有一个 vtable,但 new 方法只是一个不同的方法,它恰好具有相同的名称。编译器根据接收器的静态类型选择要使用的 vtable 插槽。 ((C)foo).GetName() 将始终调用 C.GetNameC.GetName 的覆盖(如果它是虚拟的),((A)foo).GetName() 将始终调用 A.GetName 或其覆盖之一。这里C.GetName 及其覆盖不是A.GetName 的覆盖,它们是不同的方法。

以上是关于为啥这个多态 C# 代码会打印它的功能?的主要内容,如果未能解决你的问题,请参考以下文章

Java多态的总结

C# Winform想做打印表单功能,但是打印预览总是显示空白页,请Winform大神指教,不用连接数据库

为啥打印功能没有在正确的时间运行? [复制]

为啥我的打印功能不起作用?链表

面性对象和继承

无法实例化邮件功能。为啥会出现这个错误