关于c#:CLR会检查整个继承链以确定要调用哪个虚方法吗?

Will CLR check the whole inheritance chain to determine which virtual method to call?

继承链如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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");
        }
    }

然后:

1
2
3
4
5
6
7
8
9
class Program
    {
        static void Main(string[] args)
        {
            A tan = new D();
            tan.Foo();
            Console.Read();
        }
    }

结果是,调用了类B中的foo()方法。

但在参考文献中:

When a virtual method is invoked, the run-time type of the object is
checked for an overriding member. The overriding member in the most
derived class is called, which might be the original member, if no
derived class has overridden the member.

在我的逻辑中,clr首先发现Foo()是一个虚拟方法,它查找运行时类型D的方法表,然后发现这个最派生的类中有一个重写的成员,它应该调用它,并且永远不会意识到继承链中有new Foo()

我的逻辑怎么了?


艾米的回答是正确的。下面是我如何看待这个问题。

虚拟方法是可以包含方法的槽。

当被要求执行重载解析时,编译器决定在编译时使用哪个槽。但是运行时决定了该槽中实际是什么方法。

现在考虑到这一点,让我们看看您的例子。

  • A有一个用于Foo的插槽。
  • B有一个槽位给Foo继承来自A
  • C有两个用于Foo的插槽。一个继承自B,和一个新的。你说你想要一个叫foo的新槽,所以你得到了。
  • D有两个槽供Foo使用,继承自C

这就是吃角子老虎机。那么,这些槽里有什么?

  • A中,A.Foo进入插槽。
  • B中,B.Foo进入插槽。
  • C中,B.Foo进入第一槽,C.Foo进入第二槽。记住,这些插槽完全不同。你说你想要两个同名的吃角子老虎机,所以这就是你想要的。如果这让人困惑,那就是你的问题。如果你这样做很痛,就不要这样做。
  • D中,B.Foo进入第一槽,D.Foo进入第二槽。

那么现在你的电话怎么了?

编译器的原因是您在编译时类型A上调用Foo,因此它在A上找到第一个(并且仅找到)Foo插槽。

在运行时,该插槽的内容是B.Foo

所以这就是所谓的。

现在明白了吗?


When a virtual method is invoked, the run-time type of the object is checked for an overriding member. The overriding member in the most derived class is called, which might be the original member, if no derived class has overridden the member.

你从错误的地方出发。您的变量是A类型,包含D的一个实例,因此使用的虚拟表是A的1。在上面的文本之后,我们检查是否存在重写成员。我们在B找到一个。C不计数,因为它不是重写的,而是隐藏基方法。由于D取代C,而不是AB,它也不算数。我们正在查找最派生类中的重写成员。

所以找到的方法是B.Foo()

如果更改C,使其覆盖而不是阴影,则找到的方法将是D,因为它是最派生的覆盖成员。

如果将对象改为BC的实例,则B.Foo()仍然是所选的覆盖。澄清一下,这就是我的意思:

1
2
A tan = new B();
tan.Foo();    // which foo is called?  Why, the best Foo of course!  B!

之所以调用B是因为我们搜索的继承链跨越了A(变量类型)到B(运行时类型)。CD不再是该链的一部分,也不再是虚拟表的一部分。

如果我们改为将代码改为:

1
2
C tan = new D();
tan.Foo();  // which foo, which foo?

我们搜索的继承链从CDD是一个压倒一切的成员,所以叫Foo

假设您添加了另一个继承A的类Q,以及继承Q的类R,等等。你有两个继承的分支,对吧?在搜索大多数派生类型时选择哪个?按照从A(变量类型)到D(运行时类型)的路径操作。

我希望这是有道理的。

1不是字面意思。虚拟表属于D,因为它是运行时类型,并且包含继承链中的所有内容,但是它很有用,并且更容易将A视为起点。毕竟,您正在搜索派生类型。