关于虚拟/新/覆盖的困惑

我对virtual / new / override事情有点困惑。 这是一个例子:

 class A { public virtual void mVVirtual() { Console.WriteLine("A::mVVirtual"); } } class B : A { public virtual void mVVirtual() { Console.WriteLine("B::mVVirtual"); } } class C : B { public override void mVVirtual() { Console.WriteLine("C::mVVirtual"); } } class Test { static void Main() { B b1 = new C(); b1.mVVirtual(); //C::mVVirtual ... I understand this A a2 = new C(); a2.mVVirtual(); //A::mVVirtual ... ??? } } 

我不明白为什么在第二次调用中我们得到A::mVVirtual 。 我通常用这个“算法”处理这些问题:

  1. 检查包含对象引用的变量的类型,以获取名为mVVirtual的实例方法? 没有一个……但确实有一个带有该签名和名称的虚拟方法!
  2. 虚方法? 然后让我们检查a2C )所持对象的类型,以覆盖该方法。 它有一个 – >执行C::mVVirtual

我的“算法”在哪里错了? 我真的很困惑,非常感谢一些帮助。

以下是您对虚拟方法的看法。 类的每个实例都有“框”来保存方法。 当您将方法标记为virtual它表示创建一个新的“框”并在其中放入一个方法。 在派生类中将方法标记为override时,它会保留基类中的“框”,但会在其中添加新方法。

所以这里有一个A类和一个名为mVVirtual的方法,标记为virtual 。 这表示创建一个名为mVVirtual的新“盒子”并在其中放置一个定义方法

 Console.WriteLine("A::mVVirtual"); 

然后,您有一个派生类B和一个名为mVVirtual的方法,该方法被标记为virtual 。 这表示创建一个名为mVVirtual的新“盒子”并在其中放置一个定义方法

 Console.WriteLine("B::mVVirtual"); 

特别是,从Ainheritance的“盒子”是隐藏的! 输入为B的对象或从B派生的类不能看到它。

然后,您有一个派生类C和一个名为mVVirtual的方法,该方法被标记为override 。 这就是把名为mVVirtual的“盒子”从Binheritance而来,并在其中添加了一个不同的定义方法

 Console.WriteLine("C::mVVirtual"); 

现在,当你有

 B b1 = new C(); b1.mVVirtual(); 

你告诉编译器b1B这样b1.mVVirtual()在“box” mVVirtual查找并找到定义的方法

 Console.WriteLine("C::mVVirtual"); 

因为b1实际上是C ,这就是C盒实例中“盒子” mVVirtual中的内容。

但是当你有

 A a2 = new C(); a2.mVVirtual(); 

你告诉编译器a2A ,所以它在“框”中查找并查找

 Console.WriteLine("A::mVVirtual"); 

编译器无法知道a2实际上是C (你已经将它键入为A ),因此它不知道a2实际上是一个类的实例,该类是从隐藏了“box” mVVirtual定义的类派生的由A 它知道的是A有一个名为mVVirtual的“盒子”,因此它会发出代码来调用该“盒子”中的方法。

所以,试着简洁地说:

 class A { public virtual void mVVirtual() { Console.WriteLine("A::mVVirtual"); } } 

定义一个具有全名A::mVVirtual的“框”的类,但您可以通过名称mVVirtual

 class B : A { // "new" method; compiler will tell you that this should be marked "new" for clarity. public virtual void mVVirtual() { Console.WriteLine("B::mVVirtual"); } } 

定义一个具有全名B::mVVirtual的“box”的类,但您可以通过名称mVVirtual 。 参考B.mVVirtual不会引用全名为A::mVVirtual的“盒子”; 键入为B s的对象(或从B派生的类)不能看到“box”。

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

定义一个类,它使用全名B::mVVirtual的“box”,并在其中放入一个不同的方法。

然后

 A a2 = new C(); a2.mVVirtual(); 

a2A所以a2.mVVirtual在“box”中查找全名A::mVVirtual并调用该“box”中的方法。 这就是你看到的原因

 A::mVVirtual 

在控制台上。

还有另外两种方法注释器。 abstract使一个新的“框”没有在“框”中放置方法定义。 new创建一个新的“box”并将方法定义放在“box”中,但不允许派生类将自己的方法定义放在“box”中(如果你想这样做,则使用virtual )。

很抱歉啰嗦但我希望有所帮助。

更新:有关此语言function的更多信息,请参阅此处的后续问题: 有关Virtual / new … plus接口的更多信息!

杰森的回答是正确的。 总而言之,更简洁一点。

你有三种方法。 称他们为MA,MB和MC。

你有两个“盒子”,或者,通常称为插槽。 我们会坚持杰森的命名法。 称它们为BOX1和BOX2。

“A”定义BOX1。

“B”定义BOX2。

“C”没有定义框; 它重用了BOX2。

当你说“新A()”时,BOX1用MA填写。

当你说“new B()”时,BOX1用MA填充,BOX2用MB填充。

当你说“new C()”时,用MA填充BOX1,用MC填充BOX2。

现在假设您有一个类型为A的变量,并调用该方法。 原因就像编译器一样。 编译器说“类型A上是否有与此名称匹配的框?” 是的,有一个:BOX1。 因此,编译器生成对BOX1内容的调用。

正如我们所见,BOX1的内容始终是MA,因此无论变量是否实际持有对A,B或C的引用,都始终调用MA。

现在假设您有一个B类型的变量,并调用该方法。 再次,像编译器一样思考。 编译器说“类型B上是否有与此名称匹配的框?” 是的,有两个符合名称的盒子。 编译器说“这两者中哪一个与B更紧密相关?” 答案是BOX2,因为B声明了BOX2。 因此,编译器生成对BOX2的调用。

如果变量包含B,则将调用MB,因为在B中,BOX2包含MB。 如果变量包含C,则调用MC,因为在C中,BOX2包含MC。

那现在清楚了吗? 请记住, 重载决议只选择框 。 该框的内容取决于运行时的对象。

你有隐藏的警告吗? 当我做你做的事情时,我得到这个警告:

‘ProjectName.ClassName.B.mVVirtual()’隐藏inheritance的成员’ProjectName.ClassName.A.mVVirtual()’。 要使当前成员覆盖该实现,请添加override关键字。 否则添加新关键字。

如果您在B类中使用了override ,则不会出现此问题; 两种情况都会给你“C :: mVVirtual”。 由于您没有在B类中使用override ,因此在方法前面有一个隐含的 new 。 这打破了inheritance链。 您的代码正在调用类型A上的方法,并且由于隐式new ,没有覆盖该方法的inheritance类。 所以它必须调用A类的实现。

想到它的最佳方式是虚方法使用对象的实际 (或具体 )类型来决定要执行的实现,其中非虚方法使用您用来访问方法来决定的变量的声明类型哪个运行…

覆盖意味着您正在编写一个方法,该方法将“替换”inheritance链上方的虚拟或抽象方法(具有相同名称/签名)的实现。

当链上存在具有相同名称/签名的非虚方法时,将使用new ,您要添加的方法将替换为…

区别如下

 class base { public virtual void foo() { Console.write("base.foo"); } } class derived { public override void foo() { Console.write("derived.foo"); } } base b = new base(); b.foo() // prints "base.foo" // no issue b is a base and variable is a base base b = new derived(); b.foo(); // prints "derived.foo" because concrete tyoe is derived, not base 

 class base { public void foo() { Console.write("base.foo"); } } class derived { public new void foo() { Console.write("derived.foo"); } } base b = new base(); b.foo() // prints "base.foo" // no issue b is a base and variable is a base base b = new derived(); b.foo(); // prints "base.foo" because variable b is base. derived d = b as derived; d.foo() // prints "derived.foo" - now variable d is declared as derived. 

你的第二个调用打印A::mvVirtual因为该方法( mVVirtual )实际上不是虚拟的(尽管它的名称),因为它有新的说明符……所以它根据变量类型决定,即A.

为了解释技术上的情况,Every Type有一个“方法表”,其中包含指向该类型中所有方法的指针。 (它不是具有此表的类型的实例,它是TYPE本身。)每个类型的方法表首先使用所有可访问的虚拟方法构建,从开头的object (最靠近inheritance链)到最后在类型本身中声明的虚方法。 然后,在表示所有虚拟方法之后,再次从object任何非虚方法添加所有非虚方法,首先,一直添加到Type本身中的任何非虚方法。 该表以这种方式构造,以便所有虚拟元数据的偏移量在所有派生类的方法表中都是相同的,因为编译器可以从声明为其他类型的变量调用这些方法,甚至从在基础中声明和实现的其他方法中的代码调用具体类的类型。

当编译器解析虚拟方法调用时,它将转到方法表中以获取对象本身的类型( 具体类型),而对于非虚拟调用,它将转到方法表以获取变量的声明类型。 因此,如果您调用虚方法,即使是基类型中的代码,如果实际的具体类型是从此基类型派生的类型,编译器将转到该具体类型的方法表。

如果调用非虚方法,(无论inheritance改变了实际对象的类型可能有多远),编译器访问方法表以获取变量; es’声明的类型。 这个表中没有任何衍生类型在链条中。

这就是我理解的方式

A是基类
BinheritanceA但不覆盖它
CinheritanceB但确实覆盖它

由于您声明A但初始化C,它将忽略覆盖,因为基类是A而A永远不会从B覆盖。