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

Bru*_*uno 8 c# virtual overriding keyword new-operator

我对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 ... ???
    }
}
Run Code Online (Sandbox Code Playgroud)

我不明白为什么在第二次通话中我们得到了A::mVVirtual.我通常用这个"算法"处理这些问题:

  1. 对于一个名为的实例方法,检查保存对象引用的变量的类型mVVirtual?没有一个......但确实有一个带有该签名和名称的虚拟方法!
  2. 虚方法?然后让我们检查a2(C)所持有的对象的类型,以覆盖该方法.它有一个 - >执行C::mVVirtual!

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

jas*_*son 11

以下是您对虚拟方法的看法.类的每个实例都有"框"来保存方法.当你标记一个方法,就像virtual它说一个新的"盒子"并在其中放入一个方法.将方法标记为override派生类时,它会保留基类中的"框",但会在其中添加新方法.

所以这里有一个类A和一个名为的方法mVVirtual,标记为virtual.这就是说一个新的"盒子"命​​名mVVirtual并在其中放入一个定义的方法

Console.WriteLine("A::mVVirtual"); 
Run Code Online (Sandbox Code Playgroud)

然后你有一个派生类B和一个mVVirtual标记为的方法virtual.这就是说一个新的"盒子"命​​名mVVirtual并在其中放入一个定义的方法

Console.WriteLine("B::mVVirtual"); 
Run Code Online (Sandbox Code Playgroud)

特别是,继承自的"盒子" A是隐藏的!类型为Bs 的对象或派生自的类无法看到它B.

然后你有一个派生类C和一个mVVirtual标记为的方法override.这就是说取名为"box"的名称mVVirtual继承B自然,并在定义中加入一个不同的方法

Console.WriteLine("C::mVVirtual"); 
Run Code Online (Sandbox Code Playgroud)

现在,当你有

B b1 = new C(); 
b1.mVVirtual();
Run Code Online (Sandbox Code Playgroud)

你告诉编译器b1B这样的,b1.mVVirtual()在"框"中mVVirtual查找并找到带有定义的方法

Console.WriteLine("C::mVVirtual"); 
Run Code Online (Sandbox Code Playgroud)

因为b1它真的是一个C,这就是"盒子" mVVirtual中的实例C.

但是当你有

A a2 = new C(); 
a2.mVVirtual();
Run Code Online (Sandbox Code Playgroud)

你告诉编译器a2是一个A,所以它在"框"中查找并找到

Console.WriteLine("A::mVVirtual");
Run Code Online (Sandbox Code Playgroud)

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

所以,试着简洁地说:

class A {
    public virtual void mVVirtual() { Console.WriteLine("A::mVVirtual"); }
}  
Run Code Online (Sandbox Code Playgroud)

定义一个具有全名"box" 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"); }
}  
Run Code Online (Sandbox Code Playgroud)

定义一个具有全名"box" B::mVVirtual但可以通过名称引用的类mVVirtual.参考B.mVVirtual将不提及带有全名的"盒子" A::mVVirtual; 输入的对象B(或从中派生的类B)不能看到"box" .

class C : B
{
    public override void mVVirtual() { Console.WriteLine("C::mVVirtual"); }
}  
Run Code Online (Sandbox Code Playgroud)

定义一个类,该类使用全名"box" B::mVVirtual并在其中放入不同的方法.

然后

A a2 = new C(); 
a2.mVVirtual();
Run Code Online (Sandbox Code Playgroud)

a2Aa2.mVVirtual看在"盒子"用全名A::mVVirtual,并调用在"盒子"的方法.这就是你看到的原因

A::mVVirtual
Run Code Online (Sandbox Code Playgroud)

在控制台上.

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

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


Eri*_*ert 6

更新:有关此语言功能的更多信息,请参阅此处的后续问题: 有关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.

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

  • @Posto:"base"调用是非虚拟调用.编译器直接生成对基类方法的调用,而不是通过查找虚方法槽的内容来进行间接调用. (2认同)

Cha*_*ana 1

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

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

当链上有一个具有相同名称/签名的非虚拟方法时,使用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
Run Code Online (Sandbox Code Playgroud)

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. 
Run Code Online (Sandbox Code Playgroud)

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

为了解释技术上发生的情况,每个类型都有一个“方法表”,其中包含指向该类型中所有方法的指针。(它不是具有此表的类型的实例,而是 TYPE本身。)每个类型的方法表首先由所有可访问的虚拟方法构成,从object开头的(继承链的最上面)到虚拟方法在类型本身的末尾声明的方法。然后,在表示所有虚拟方法之后,再次将所有非虚拟方法从 中的任何非虚拟方法添加object到类型本身中的任何非虚拟方法。该表的结构如此,以便所有虚拟方法的偏移量在所有派生类的方法表中都相同,因为编译器可以从声明为其他类型的变量调用这些方法,甚至可以从在基类中声明和实现的其他方法中的代码调用这些方法具体类的类型。

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

如果调用非虚拟方法(无论继承更改实际对象类型的程度如何),编译器将访问 variab;es 声明类型的方法表。该表没有任何来自链下游的任何派生类型的内容。