继承链如下:
class A
{
public virtual void Foo()
{
Console.WriteLine("A's method");
}
}
class B:A
{
public override void Foo()
{
Console.WriteLine("B's method");
}
}
class C:B
{
public new virtual void Foo()
{
Console.WriteLine("C's method");
}
}
class D:C
{
public override void Foo()
{
Console.WriteLine("D's method");
}
}
然后:
class Program
{
static void Main(string[] args)
{
A tan = new D();
tan.Foo();
Console.Read();
}
}
结果是,调用了B类中的方法foo()。
但在reference:
调用虚方法时,对象的运行时类型为 检查一个压倒一切的成员。最重要的成员 调用派生类,如果不是,则可以是原始成员 派生类已重写成员。
在我的逻辑中,CLR首先发现Foo()
是一个虚方法,它查看D
的方法表,运行时类型,然后它发现在这个派生程序中有一个重写成员class,它应该调用它,永远不会意识到继承链中有new Foo()
。
我的逻辑出了什么问题?
答案 0 :(得分:34)
艾米的回答是正确的。以下是我喜欢看这个问题的方法。
虚拟方法是一个插槽,可以包含方法。
当要求进行重载解析时,编译器确定在编译时使用的插槽 。但运行时确定该插槽中实际的方法。
现在请记住你的例子。
A
有Foo
个插槽。 B
有一个Foo
的插槽,已继承
来自A
。 C
有两个Foo
位。一个继承自B
,和
一个新的。你说你想要一个名为Foo 的 new slot ,所以你得到了它。 D
有两个Foo
位置,继承自C
。那是插槽。那么,那些插槽里有什么?
A
中,A.Foo
进入广告位。B
中,B.Foo
进入广告位。C
中,B.Foo
进入第一个广告位,C.Foo
进入第二个广告位。请记住,这些插槽完全不同。你说你想要两个同名的插槽,这就是你得到的。如果那令人困惑,那就是你的问题。如果你这样做会伤害,不要这样做。D
中,B.Foo
进入第一个广告位,D.Foo
进入第二个广告位。那么现在你的电话会怎么样?
编译器的原因是您在编译时类型Foo
上调用A
,因此它会找到Foo
上的第一个(也是唯一的)A
个插槽。
在运行时,该插槽的内容为B.Foo
。
这就是所谓的。
现在有意义吗?
答案 1 :(得分:29)
调用虚方法时,将检查对象的运行时类型是否有覆盖成员。如果没有派生类重写成员,则调用派生程度最大的类中的重写成员,该成员可能是原始成员。
你是从错误的地方开始的。您的变量的类型为A
,并且包含D
的实例,因此使用的虚拟表是A
的 1 。在上面的文字之后,我们检查一个压倒一切的成员。我们在B
中找到了一个。 C
不计算,因为它没有覆盖,它是 shadowing 基本方法。由于D
会覆盖C
,而不是A
或B
,因此它也不会重要。我们正在寻找最派生类中的重写成员。
所以找到的方法是B.Foo()
。
如果更改C
使其覆盖而不是阴影,则找到的方法将为D
,因为它是派生最多的覆盖成员。
如果您将对象更改为B
或C
的实例,B.Foo()
仍将是所选的覆盖。澄清一下,这就是我的意思:
A tan = new B();
tan.Foo(); // which foo is called? Why, the best Foo of course! B!
调用B
的原因是因为我们正在搜索的继承链从A
(变量类型)跨越到B
(运行时类型)。 C
和D
不再是该链的一部分,也不属于虚拟表。
如果我们改为将代码更改为:
C tan = new D();
tan.Foo(); // which foo, which foo?
我们搜索的继承链从C
跨越到D
。 D
有一个重要成员,因此调用Foo
。
假设您添加了继承自Q
的{{1}}和A
继承的另一个类R
,依此类推。你有两个继承分支,对吧?在搜索大多数派生类型时选择哪个?遵循从Q
(您的变量类型)到A
(运行时类型)的路径。
我希望这是有道理的。
1 不是字面意思。虚拟表属于D
,因为它是运行时类型并包含其继承链中的所有内容,但它有用且更容易将D
视为起点。毕竟,您正在搜索派生类型。