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

继承链如下:

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 的方法表,即运行时类型,然后它发现在这个派生类中有一个重写成员,它应该调用它并且永远不会意识到它有继承链中的 new Foo() .

我的逻辑出了什么问题?

回答(2)

2 years ago

调用虚方法时,将检查对象的运行时类型是否有覆盖成员 . 如果没有派生类重写成员,则调用派生程度最大的类中的重写成员,该成员可能是原始成员 .

你是从错误的地方开始的 . 您的变量类型为 A 并包含 D 的实例,因此使用的虚拟表是 A 的1.按照上面的文本,我们检查覆盖成员 . 我们在 B 找到一个 . C 不计算,因为它没有覆盖,它是阴影基本方法 . 由于 D 会覆盖 C ,而不是 AB ,因此它不会在最派生的类中查找重写成员 .

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

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

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

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

调用 B 的原因是因为我们正在搜索的继承链从 A (变量类型)跨越到 B (运行时类型) . CD 不再是该链的一部分,并且不属于虚拟表 .

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

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

我们搜索的继承链从 CD . D 有一个重写成员,因此调用 Foo .

假设您添加了另一个继承自 A 的类 Q 和继承自 QR ,依此类推 . 你有两个继承分支,对吧?在搜索大多数派生类型时选择哪个?遵循从 A (您的变量类型)到 D (运行时类型)的路径 .

我希望这是有道理的 .

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

2 years ago

艾米的回答是正确的 . 这是我喜欢看这个问题的方式 .

虚方法是可以包含方法的插槽 .

当要求进行重载解析时,编译器确定在编译时使用哪个插槽 . 但运行时确定该槽中实际的方法 .

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

  • A 有一个 Foo 的插槽 .

  • B 有一个 Foo 的插槽,继承自 A .

  • C 有两个用于 Foo 的插槽 . 一个继承自 B ,一个新的 . 你说你想要一个名为Foo的新插槽,所以你得到了它 .

  • D 有两个 Foo 插槽,继承自 C .

那是插槽 . 那么,那些插槽里有什么?

  • A 中, A.Foo 进入插槽 .

  • B 中, B.Foo 进入插槽 .

  • C 中, B.Foo 进入第一个插槽, C.Foo 进入第二个插槽 . 请记住,这些插槽完全不同 . 你说你想要两个具有相同名称的插槽,这样's what you got. If that'会让人感到困惑,如果你这么做就会伤害它 .

  • D 中, B.Foo 进入第一个插槽, D.Foo 进入第二个插槽 .

那么现在你的电话会怎么样?

编译器的原因是你在编译时类型 A 上调用了 Foo ,因此它找到 A 上的第一个(也是唯一的) Foo 插槽 .

在运行时,内容那个插槽是 B.Foo .

这就是所谓的 .

现在有道理吗?