我的烦恼是:在下面显示的代码中,它应显示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
答案 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
的构造函数也是如此。
我会回顾你提到的所有案件。我想鼓励仔细阅读。此外,请保持开放态度,因为这与某些其他语言中的情况不符。
这是覆盖方法的情况,即:
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
来访问替换的方法。
这是隐藏方法的情况,即:
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
我认为这个很清楚。
这是您执行此操作的代码的变体:
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 1,part 2和part 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#中将确切的方法插入并执行。