覆盖并在C#中的Base类构造函数中调用相同的方法

时间:2013-12-06 03:24:27

标签: c# asp.net .net c#-4.0

我的烦恼是:在下面显示的代码中,它应显示A然后B。但它会显示B,然后显示B。为什么会这样?

我的感觉是,A的构造函数在创建B的对象时首先被执行。在这种情况下,B中的方法不会正确吗?因此它应该是A.Display()并且应该得到A。此外,a.Display()应返回B,因为我们已覆盖。

所以我希望A然后B。因为它没有超载而是压倒一切。我知道这些事情的定义,我希望了解这种行为的原因以及它在内部如何运作,因为我不相信BB而是AB


代码

class A
{
    public A()
    {
        this.Display();
    }
    public virtual void Display()
    {
        Console.WriteLine("A");
    }
}

class B :A
{
    public override void Display()
    {
        Console.WriteLine("B");
    }
}

class C
{
    static void Main()
    {
        A a = new B();
        a.Display();
        Console.WriteLine();
        Console.ReadLine();
    }
}

输出

1)在派生类中覆盖Display方法会产生以下结果:

A a = new A(); // ---> AA
B a = new B(); // ---> BB // I expect AB.
A a = new B(); // ---> BB // I expect AB.

2)在派生类的Display方法中使用 NEW 关键字会产生以下结果:

B a = new B(); // ---> AB // I Expect AA here.
A a = new B(); // ---> AA
A a = new A(); // ---> AA

3)更有趣的发现是:

当我在派生构造函数中使用base.Display()并覆盖派生类中的基本方法时,它会给我BAB

至少在这方面我没有看到任何逻辑。因为,它应该给BBB

5 个答案:

答案 0 :(得分:9)

  

我的感觉是,A的构造函数在创建B的对象时首先被执行。

正确。

  

在这种情况下,B中的方法不会被点击吗?

这是不正确的。

在C ++的类似代码中,你是对的。在C ++中,有一条规则是在构造对象时构建虚拟函数调度表 。也就是说,当输入“A”构造函数时,vtable用“A”中的方法填充。当控制进入“B”监视器时,然后使用B的方法填充vtable。

在C#中并非如此。在C#中,vtable在对象从内存分配器出来之前填充,然后执行ctor,并且在此之后它不会更改。该方法的vtable插槽始终包含派生最多的方法。

因此,正如您在此处调用虚拟方法一样,非常糟糕的主意。可以调用虚方法,其中实现在ctor尚未运行的类上!因此,它可能取决于尚未初始化的州。

请注意,字段初始值设定项在所有ctor体之前运行,所以幸运的是,对更多派生类的覆盖将始终在覆盖类的字段初始值设定项之后运行。

故事的寓意是:根本不这样做。不要在ctor中调用虚方法。在C ++中,您可能会获得与预期不同的方法,在C#中,您可能会得到一个使用未初始化状态的方法。避免,避免,避免。

  

为什么我们不应该在ctor中调用虚方法?是因为我们只在vtable中得到(最新派生的)结果吗?

是。让我举一个例子来说明:

class Bravo
{
    public virtual void M() 
    {
        Console.WriteLine("Bravo!");
    }
    public Bravo()
    {
        M(); // Dangerous!
    }
}
class Delta : Bravo:
{
    DateTime creation;
    public override void M() 
    {
        Console.WriteLine(creation);
    }
    public Delta() 
    {
        creation = DateTime.Now;
    }
}

好的,所以这个程序的预期行为是当在任何M上调用Delta时,它将打印出实例创建的时间。但是new Delta()上的事件顺序是:

  • Bravo ctor运行
  • Bravo ctor致电this.M
  • M是虚拟的,this属于运行时类型Delta,因此Delta.M运行
  • Delta.M打印出未初始化的字段,该字段设置为默认时间,而不是当前时间。
  • M返回
  • Bravo ctor返回
  • Delta ctor设置字段

现在,当我说重写方法可能依赖于尚未初始化的状态时,您是否明白我的意思?在M的任何其他用法中,这都没问题,因为Delta ctor已经完成了。但是M ctor甚至在开始之前调用了Delta

答案 1 :(得分:3)

您创建对象B的实例。它使用在类A上定义的构造函数的代码,因为您没有在B中覆盖它。但实例仍为B,因此构造函数中调用的其他方法是B中定义的方法,而不是A。因此,您会在班级Display()中看到B的结果。

根据问题更新进行更新

我会尝试解释你得到的“怪异”结果。

  

覆盖时:

     

B a = new B(); // ---> BB //我期待AB。

     

a a = new B(); // ---> BB //我期待AB。

如上所述。当您覆盖子类的方法时,如果您正在使用子类的实例,则使用此方法。这是一个基本规则,所使用的方法由变量实例的类决定,而不是由用于声明变量的类决定。

  

对方法使用 new 修饰符时(隐藏继承方法)

     

B a = new B(); // ---> AB //我在这里期待AA。

现在有两种不同的行为:

  • 使用构造函数时,它使用类A中的构造函数。由于继承的方法隐藏在子类中,构造函数使用类Display()中的A方法,因此您看到A打印。

  • 稍后直接调用Display()时,变量的实例为B。因此,它使用类B上定义的方法打印B。

答案 2 :(得分:3)


初始陈述

我将从基本代码开始,我已将其调整为在LINQPad中运行(我也将其更改为Write而不是WriteLine,因为我不会保留无论如何,在解释中的新行。)

class A
{
    public A()
    {
        this.Display();
    }

    public virtual void Display()
    {
        Console.Write("A"); //changed to Write
    }
}

class B :A
{
    public override void Display()
    {
        Console.Write("B"); //changed to Write
    }
}

static void Main()
{
    A a = new B();
    a.Display();
}

输出结果为:

BB

在你最初提出的问题中,你说你期待:

AB

这里发生的事情(Szymon attempted to explain)是您正在创建B类型的对象,而类B 会覆盖方法{{1}类Display的。因此,无论何时在该对象上调用A,它都将是派生类(Display)的方法,即使是B的构造函数也是如此。


我会回顾你提到的所有案件。我想鼓励仔细阅读。此外,请保持开放态度,因为这与某些其他语言中的情况不符。


1)在派生类

中覆盖Display方法

这是覆盖方法的情况,即:

A

覆盖时,对于所有实际用途,将使用的方法是派生类的方法。将覆盖视为替换

案例1

public override void Display()
{
    Console.Write("B"); //changed to Write
}

我们没问题。

案例2

A a = new A(); // ---> AA

如上所述,在对象上调用B a = new B(); // ---> BB // I expect AB. 将始终是派生类的方法。因此,两次调用Display都会产生Display

案例3

B

这是同样混乱的变种。该对象显然属于A a = new B(); // ---> BB // I expect AB. 类型,即使您将其包含在B类型的变量中也是如此。请记住,在C#中,类型是对象的属性,而不是变量的属性。因此,结果与上述相同。


注意:您仍然可以使用A来访问替换的方法。


2)在派生类

中的Display方法中使用NEW关键字

这是隐藏方法的情况,即:

base.Display()

隐藏方法时,表示原始方法仍然可用。您可以将其视为一种不同的方法(恰好具有相同的名称和签名)。也就是说:派生类不是替换覆盖该方法。

因此,当您对对象执行(虚拟)调用时,在编译时决定使用基类的方法...不考虑派生类的方法(在实践中,它的作用不是虚拟呼叫)。

这样想:如果使用基类的变量调用方法...代码不知道存在隐藏方法的派生类,并且可以使用其中一个执行该特定调用那些对象。相反,它将使用基类的方法,无论如何。

案例1

public new void Display()
{
    Console.Write("B"); //changed to Write
}

您会看到,在编译时,构造函数中的调用被设置为使用基类的方法。那个给出了B a = new B(); // ---> AB // I Expect AA here. 。但由于变量的类型为A,编译器会知道该方法在第二次调用时是隐藏的。

案例2

B

这里,在构造函数和第二次调用中都不会使用新方法。它没有意识到这一点。

案例3

A a = new B(); // ---> AA

我认为这个很清楚。


3)使用base.Display()

这是您执行此操作的代码的变体:

A a = new A(); // ---> AA

public new void Display() { base.Display(); Console.Write("B"); //changed to Write } 将成为基类(base.Display())中的方法,无论如何。


进一步阅读

你说你想了解它是如何在内部运作的。

您可以通过阅读Microsoft's C# Spec on Virtual Methods

来深入了解

然后阅读Eric Lippert在C#中实现虚拟方法模式(part 1part 2part 3

您可能也有兴趣:


网络虚拟方法的其他解释:

答案 3 :(得分:0)

在将类a实例化为类B时,可能会让您感到困惑。如果您要调用虚拟方法,可以使用base关键字。以下代码写入A B

class A
{
    public A()
    {
        //this.Display();
    }
    public virtual void Display()
    {
        Console.WriteLine("A");
    }
}

class B : A
{
    public override void Display()
    {
        base.Display();
        Console.WriteLine("B");
    }
}

class C
{
    static void Main(string[] args)
    {
        A a = new B();
        a.Display();
        Console.WriteLine();
        Console.ReadLine();
    }
}

另请注意,您可以通过在开头设置断点然后逐行执行代码执行(F11)来查看代码显示B B的原因。

答案 4 :(得分:0)

我了解的是,在使用虚拟方法的情况下,父对象和子对象之间共享同一方法插槽。

如果是这样,那么我认为当调用对象虚拟方法时,编译器会以某种方式用适当的方法地址更新方法槽,以便在c#中将确切的方法插入并执行。