【问题标题】:Confusion about virtual/new/override关于虚拟/新/覆盖的困惑
【发布时间】:2011-01-05 03:58:06
【问题描述】:

我对@9​​87654321@/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. 虚拟方法?然后让我们检查a2 (C) 持有的对象的类型,以覆盖该方法。它有一个 -> 执行 C::mVVirtual!

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

【问题讨论】:

  • @John Gietzen:我指的是我在处理这些问题时的思路……如果你愿意,它是一种心理算法。到目前为止它对我有用,但这个问题真的很困扰我。
  • 算法 - 一组有限的明确指令,以规定的顺序执行以实现目标。在我看来,这很像一个算法,约翰。

标签: c# virtual overriding new-operator keyword


【解决方案1】:

以下是您对虚拟方法的看法。类的每个实例都有“盒子”来保存方法。当您将方法标记为virtual 时,它会说制作一个新的“盒子”并将一个方法放入其中。当您在派生类中将方法标记为override 时,它会保留基类中的“盒子”,但会在其中放置一个新方法。

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

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

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

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

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

然后你有一个派生类C 和一个名为mVVirtual 的方法被标记为override。这表示取名为 mVVirtual 的“盒子”继承自 B,并在其中定义一个不同的方法

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

现在,当你有

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

您是在告诉编译器 b1B,以便 b1.mVVirtual() 在“框”中查找 mVVirtual 并找到具有定义的方法

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

因为b1 确实是C,这就是mVVirtualC 实例的“盒子”中的内容。

但是当你有

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

您是在告诉编译器 a2A,因此它会在“框”中查找并找到

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

编译器无法知道a2 确实是C(您已将其键入为A),因此它不知道a2 确实是派生自的类的实例一个隐藏了A定义的“盒子”mVVirtual的类。它所知道的是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 的“盒子”,但您可以通过名称mVVirtual 来引用它。提及B.mVVirtual不会指全称A::mVVirtual的“盒子”;类型为 Bs 的对象(或派生自 B 的类)无法看到该“盒子”。

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

定义一个类,该类接受全名为B::mVVirtual 的“盒子”并在其中放入不同的方法。

然后

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

a2 是一个A,因此a2.mVVirtual 在全名A::mVVirtual 的“框”中查找并调用该“框”中的方法。这就是你看到的原因

A::mVVirtual

在控制台上。

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

抱歉啰嗦了,希望对你有所帮助。

【讨论】:

  • 好吧,从这个角度来看是有道理的......但是我所说的“算法”就崩溃了,因为我认为当编译器看到标记为虚拟的方法时,它会搜索方法的覆盖版本的对象类型,如果存在,则执行它!如果它不存在,我们会一路爬过层级寻找覆盖...我不知道我是否正确地解释了自己...
  • 你解释得很好;我理解你的想法。我的解释表明,这种想法有点不对劲。
  • 但是对于大多数情况,我的想法是好的,对吧?只有在这种情况下,它有点偏离。
  • 如果不可能隐藏“盒子”,你的想法会很好。
  • 澄清一下,它不是具有这些“框”的类型的实例,而是类型本身。每个类型都有一个“方法表”,其中包含指向该类型中所有方法的指针。每个类型的方法表首先由虚拟方法构成,从Object(继承链的最上端)开始。到类型本身中声明的虚拟方法。之后是非虚拟方法。当编译器解析虚拟方法调用时,它会转到方法表中查找对象本身的类型,而对于非虚拟调用,它会转到方法表中查找变量的声明类型。
【解决方案2】:

更新:有关此语言功能的更多信息,请参阅此处的后续问题:More about Virtual / new...plus interfaces!

杰森的回答是正确的。总结的更简洁一些。

你有三种方法。称它们为 MA、MB 和 MC。

您有两个“盒子”,或者通常称为插槽。我们将坚持使用 Jason 的命名法。称它们为 BOX1 和 BOX2。

“A”定义了 BOX1。

“B”定义了 BOX2。

"C" 定义无框;它重用了 BOX2。

当你说“new A()”时,BOX1 填入 MA。

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

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

现在假设您有一个类型为 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。

现在清楚了吗?请记住,重载分辨率只是选择框。盒子的内容取决于运行时的对象。

【讨论】:

  • 知道了......杰森的回答真的为我打开了它!谢谢大家 ;)
  • @EricLippert 很好的解释。只是有一个困惑,当你说“new C()”时,BOX1 用 MA 填充,BOX2 用 MC 填充。 所以 BOX2(B 类盒子)用 MC 填充,但是如果我们这样做 class C : B { public override void mVVirtual() { base.mVVirtual(); Console.WriteLine("C::mVVirtual"); } } 它将打印 B::mVVirtual C::mVVirtual 但我认为 Box2 充满了 MC。
  • @Posto:“基本”调用是非虚拟调用。编译器直接生成对基类方法的调用,而不是通过查找虚方法槽的内容来进行间接调用。
  • 好的,你能指出任何资源(在线文档等),我可以在其中了解非虚拟或虚拟呼叫的详细信息
【解决方案3】:

您是否隐藏了警告?当我做你所做的事情时,我会收到以下警告:

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

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

【讨论】:

  • 是的,我有警告,但请看,这是一道考试题,问题与代码是否抛出警告无关......它只需要正确编译即可。问题是:控制台上的输出是什么,为什么?我知道在 B 类中我们应该做 new void mVVirtual()...,但我想如果我们让它是虚拟的,它会产生相同的效果(在类型的方法表上创建一个新插槽),但当然发出警告。
  • 在 B 类中,您应该使用 newoverride。给出的代码不使用任何一个。在这种情况下,编译器可能会引发错误;但相反,它选择假设您的意思是new,并显示警告。 B 中 virtual 的唯一作用是让您覆盖 C 中的新方法;它与 A 根本没有任何联系。
  • 我在 B 类中使用了 new 并且输出仍然是 A::mVVirtual...我仍然不明白为什么 :S
  • @Bruno:因为在这种情况下您想要将 B::mVVirtual 标记为 virtual override。当您考虑修饰符的组合含义时,您会得到您期望的行为。只是因为A的实现是virtual,所以当你声明B的实现为override时,它会自动继承virtual修饰符,所以不需要多余的指定。
【解决方案4】:

最好的想法是虚拟方法使用 actual(或 concrete)类型决定执行什么实现的对象,其中非虚拟方法使用'声明的变量的类型来访问该方法来决定运行哪个...

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

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) 实际上是 虚拟的(尽管它的名称),因为它具有 new说明符...所以它根据变量类型来决定,即A。

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

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

如果您调用非虚拟方法,(无论继承更改实际对象的类型可能有多远),编译器都会访问变量声明类型的方法表。该表在链中的任何派生类型中没有

【讨论】:

  • 关键是第二种情况,因为new,他在调用非虚方法...
【解决方案5】:

我是这样理解的

A 是基类
B 继承 A 但不覆盖它
C 继承 B 但确实覆盖它

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

【讨论】:

    猜你喜欢
    • 2015-05-14
    • 1970-01-01
    • 2016-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-26
    • 2012-05-22
    • 2014-10-15
    相关资源
    最近更新 更多