【问题标题】:Will CLR check the whole inheritance chain to determine which virtual method to call?CLR 会检查整个继承链以确定调用哪个虚拟方法吗?
【发布时间】:2017-08-25 00:42:43
【问题描述】:

继承链如下:

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()

我的逻辑有什么问题?

【问题讨论】:

    标签: c# .net inheritance


    【解决方案1】:

    艾米的回答是正确的。以下是我对这个问题的看法。

    虚拟方法是一个可以包含方法的槽

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

    考虑到这一点,让我们看看您的示例。

    • A 有一个用于Foo 的插槽。
    • B 有一个用于Foo 的插槽,继承 来自A
    • C 有两个用于Foo 的插槽。一个继承自B,和 一个新的。你说你想要一个 new 插槽名为 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

    所以这就是所谓的。

    现在有意义吗?

    【讨论】:

    • 如果有一个接口ITest,其中包含一个方法Bar()。如果基类及其派生类都实现了这个接口,那么派生类中有多少个Bar()的槽?
    • @ClaudeTan:一个接口没有添加一个新的槽。接口要求存在这样的插槽。现在,调用接口方法时选择哪个槽的确切细节很复杂;详细信息请搜索“接口重新实现”。
    • 我不完全确定这有多少是一个类比,但是这将如何应用于调用方法的基本实现?例如,如果B 的唯一Foo 插槽包含B.Foo,那么它去哪里找到A.Foo
    • @Rob 包含方法的槽是一种简化,它们实际上包含指向方法的指针。如果你在 B 的方法中调用 Foo(),它会调用 slot Foo 指向的方法。但是,如果显式调用 A.Foo(),编译器会忽略插槽并插入对方法 A.Foo() 的直接调用。
    • @Tomeamis:这是一个疯狂的场景。您有三个 DLL,X.DLL、Y.DLL 和 Z.DLL,分别具有 X、Y 和 Z 类,以及 Y:X 和 Z:Y。X 具有虚拟方法 Foo。 Y 不会覆盖 Foo。 Z 覆盖 Foo 并调用 base.Foo。所以 X.Foo 被称为。现在假设您将 Y.DLL 替换为 不同 Y.DLL,其中 Y 确实 覆盖 X.Foo。您无需重新编译 Z.DLL。当你运行 Z.Foo 时会发生什么?它叫 X.Foo 还是 Y.Foo?你认为应该会发生什么?实际上会发生什么,为什么会发生?
    【解决方案2】:

    当调用虚拟方法时,会检查对象的运行时类型是否存在覆盖成员。调用最派生类中的重写成员,如果没有派生类重写该成员,则该成员可能是原始成员。

    你从错误的地方开始。你的变量是A 类型并且包含D 的一个实例,所以使用的虚拟表是A1。在上面的文本之后,我们检查一个覆盖成员。我们在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?
    

    我们搜索的继承链从CDD 有一个覆盖成员,所以它的 Foo 被调用。

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

    我希望这是有道理的。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-11-30
      • 2015-09-09
      • 1970-01-01
      • 1970-01-01
      • 2011-06-07
      • 1970-01-01
      • 1970-01-01
      • 2017-05-09
      相关资源
      最近更新 更多