【问题标题】:Overriding and calling same method in Base class constructor in C#在 C# 的基类构造函数中覆盖和调用相同的方法
【发布时间】:2026-01-07 04:10:02
【问题描述】:

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

我的感觉是,A 的构造函数在创建B 的对象时首先被执行。在那种情况下,B 中的方法不会被击中吧?所以它应该是A.Display(),结果应该是A。另外,a.Display() 应该返回 B,因为我们有覆盖。

所以我希望A 然后是B。因为它不是重载而是压倒一切。我知道这些东西的定义,我希望了解这种行为的原因以及它在内部是如何工作的,因为我不相信BBAB


代码

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

【问题讨论】:

  • 当A的构造函数运行时,你实际上在B中。因此,由于Display被覆盖,this.Display返回B
  • @OldProgrammer:一点也不。我不需要定义重载或覆盖。我完全理解这一点。但我不理解这种行为。我期待A。我不期待B。我首先期待A。然后 B.
  • 正如@Szymon 在他的回答中解释的那样,当你有一个B 类型的对象时,A 中的构造函数会调用DisplayB 方法。之所以如此,是因为您正在覆盖而不是隐藏基类的方法。因此,在 B 类型的对象中,A 类中的 Display 方法“不存在”*(因为您确实覆盖了它),如果您隐藏了它,它的行为将与您预期的一样。 *:根据 Zerkey 的回答,您仍然可以通过 base.Display 获得它。
  • @Theraot:不不不,我对很少的输出感到满意,但对其他一些奇怪的输出不满意。直到现在我以为我知道压倒一切。现在非常好笑。这现在的行为有所不同。请查看我在上面解释的所有场景,并强调了我有顾虑的地方。 (第 1、2、3 点)。
  • 我已经对您的问题进行了编辑,我会在答案中查看每个输出。顺便说一句:没有“重载”(至少不像 C# 中所理解的那样),使用关键字“new”不是重载它是隐藏的。

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


【解决方案1】:

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

正确。

那样的话,B中的方法不会被命中吧?

这是不正确的。

在 C++ 中的类似代码中,您是正确的。在 C++ 中,有一条规则是在构建对象时构建虚函数调度表。也就是说,当进入“A”构造函数时,vtable 被“A”中的方法填充。当控制权转到“B”ctor 时,vtable 会被 B 的方法填充。

在 C# 中情况并非如此。在 C# 中,vtable 在对象从内存分配器中出来的那一刻被填充,在任何一个 ctor 执行之前,之后它就不会改变。方法的 vtable 槽始终包含派生最多的方法。

因此,像您在这里所做的那样在 ctor 中调用虚方法是一个非常糟糕的主意。可以在其 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;
    }
}

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

  • Bravoctor 运行
  • Bravo ctor 致电 this.M
  • M 是虚拟的,this 是运行时类型 Delta 所以 Delta.M 运行
  • Delta.M 打印出 uninitialized 字段,该字段设置为默认时间,而不是当前时间。
  • M返回
  • Bravoctor 返回
  • Deltactor 设置字段

现在你明白我所说的覆盖方法可能依赖于尚未初始化的状态的意思了吗?在M 的任何其他用法中,这很好,因为Delta ctor 已经完成。但是这里 MDelta ctor 开始之前就被调用了!

【讨论】:

  • 非常感谢 Eric 的精彩解释。我从来不知道 vtables 和状态。我不会在构造函数中使用这样的虚方法。我将阅读 Theraot 推荐的所有 3 部分教程。非常感谢您的时间和帮助:) :)
  • Eric,还有一个小或愚蠢的澄清,也许我无法完全理解它。为什么我们不应该在 ctor 中调用虚方法?是因为我们总是在 vtable 中只得到(最新派生的)结果吗?在我的帖子中的问题场景中,哪个让我感到困惑?
  • @Divine Check:*.com/questions/119506/… 摘要:1) 为派生类打开改变基类字段初始化的大门。 2)允许派生类中的方法在派生类的字段尚未初始化(派生类的构造函数尚未运行)时运行。 3)派生类的派生类可能会通过覆盖虚拟方法来搞乱派生类所做的事情。这不是一件彻底的坏事,它有时很有用(我自己也用过),但可能很难做到。
  • @Divine:不客气!我已更新答案以解决您的后续问题。
  • @Theraot:谢谢你的链接和解释,这很清楚:) :)
【解决方案2】:

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

根据问题的更新进行更新

我将尝试解释您得到的“奇怪”结果。

覆盖时:

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

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

这在上面已经介绍过了。当您覆盖子类的方法时,如果您使用的是子类的实例,则使用此方法。这是一个基本规则,使用的方法由变量实例的类决定,而不是由用于声明变量的类决定。

当为方法使用new修饰符时(隐藏继承的方法)

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

现在这里有两种不同的行为:

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

  • 以后直接调用Display()时,变量的实例是B。出于这个原因,它使用在类B 上定义的方法来打印B。

【讨论】:

  • 伙计,你错了。我在派生类中将覆盖设置为新的。当我说 A a = new B() 时,它给了我 AA,当我说 B a = new B() 时,它给了我 AB。我觉得很好笑。怎么会这么傻,你甚至都没有先创建 B 的对象。当它引用 A 的构造函数时,您创建 A 的对象。
  • 等我回家后让我再仔细看看。
  • 谢谢。同样,当我说 A a = new A() 时,它给了我 AA。我真的觉得它很奇怪,无法理解它是如何工作的。无论如何,非常感谢您的宝贵时间。让我们弄清楚发生了什么。
  • 感谢 Szymon,我明白了覆盖的定义。然而,我所有的疑问是,尽管在我上面的代码中的所有这些执行中首先受到打击的是基类构造函数,因为我没有任何其他私有字段。现在,我的问题是为什么 cons 调用应该产生 A 但在某些情况下会产生其他结果。现在我很清楚,CLR/.NET 中的 vtables 负责存储最新的覆盖结果。当我们调用该方法时,看起来 vtable 给出了最新的。请关注我也在关注的 Eric Lippert 的帖子。 vtable 是个谜。
【解决方案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覆盖Display 的方法A。因此,每当您对该对象调用 Display 时,它将是派生类 (B) 的方法,即使来自 A 的构造函数。


我会复习你提到的所有案例。我想鼓励您仔细阅读。另外,请保持开放的态度,因为这与某些其他语言中发生的情况不符。


1) 在派生类中重写 Display 方法

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

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

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

案例 1

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

我们没问题。

案例 2

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

如上所述,在对象上调用Display 将始终是派生类上的方法。因此,对Display 的两次调用都会产生B

案例 3

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

这是相同混淆的变体。该对象显然属于B 类型,即使您将它放在A 类型的变量中。请记住,在 C# 中,类型是对象的属性而不是变量的属性。所以,结果和上面一样。


注意:您仍然可以使用base.Display() 访问被替换的方法。


2) 在派生类的Display方法中使用NEW关键字

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

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

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

因此,当您对对象进行(虚拟)调用时,在编译时决定将使用基类的方法......派生类的方法没有被考虑在内(实际上,它不是虚拟呼叫)。

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

案例 1

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

你看,在编译时,构造函数中的调用被设置为使用基类的方法。那个给A。但由于变量的类型为B,编译器知道该方法在第二次调用时被隐藏了。

案例 2

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

在这里,无论是在构造函数中还是在第二次调用中,它都不会使用新方法。它不知道它。

案例 3

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

我认为这一点很清楚。


3) 使用 base.Display()

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

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

base.Display() 将成为基类 (A) 中的方法,无论如何。


进一步阅读

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

您可以通过阅读Microsoft's C# Spec on Virtual Methods 来深入了解

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

您可能也有兴趣:


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

【讨论】:

  • 非常感谢 Theraot 对每一种行为进行如此详细的解释,这对我很有帮助。非常感谢您提供更多关于阅读指南的指导,我将阅读所有这些链接并更好地理解它。 vtable 概念我不知道,这是我想知道的(如谁对这种行为负责)。非常感谢您,您的深入著作对我理解很有帮助。谢谢你的时间 :) 干杯 :)
【解决方案4】:

将类命名为 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 的原因。

【讨论】:

  • "在将其实例化为 B 类的同时命名该类" 这听起来是错误的。一个人做了他/她所做的,并问他/她想要什么。它只是将变量声明为基本类型并分配一个具体类型,这是正常的。您已更改代码,因此您的答案无效且错误
  • 好吧,我当然改了代码,结果那家伙提到了三遍他想要A B,所以我注释了一行并添加了一行代码。我只是建议他可能将a 的名称更改为b,因为a 实际上是B 类,正如调试器会告诉你的那样。
  • 同意第一部分,但A a = new B(); 是正确的代码。将具体实例分配给基类型变量是好的
  • 他的代码是这样写的:Animal animal = new Cat() 在后面的代码中,当你引用animal 时,它实际上是一个Cat,最好写成Animal cat = new Cat() ...这就是我的意思。
【解决方案5】:

我的理解是,在虚拟方法的情况下,父对象和子对象共享相同的方法槽。

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

【讨论】: