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

Cla*_*Tan 21 .net c# inheritance

继承链如下:

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");
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后:

class Program
    {
        static void Main(string[] args)
        {
            A tan = new D();
            tan.Foo();
            Console.Read();
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

但在参考中:

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

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

我的逻辑出了什么问题?

Eri*_*ert 34

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

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

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

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

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

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

  • 在一个A,A.Foo进入插槽.
  • 在a B,B.Foo进入插槽.
  • 在a中C,B.Foo进入第一个插槽并C.Foo进入第二个插槽.请记住,这些插槽完全不同.你说你想要两个同名的插槽,这就是你得到的.如果那令人困惑,那就是你的问题.如果你这样做会伤害它,不要这样做.
  • 在a中D,B.Foo进入第一个插槽并D.Foo进入第二个插槽.

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

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

在运行时,该插槽的内容是B.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?您认为*应该*发生什么?实际**会发生什么,为什么? (6认同)
  • @ClaudeTan:接口不添加新插槽.接口*要求存在这样的插槽*.现在,调用接口方法时拾取的插槽的确切详细信息很复杂; 在"界面重新实现"中搜索详细信息. (2认同)

Amy*_*Amy 29

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

你是从错误的地方开始的.您的变量是类型A并包含实例D,因此使用的虚拟表是A's 1.在上面的文字之后,我们检查一个压倒一切的成员.我们找到一个B. C不计算因为它没有覆盖,它是阴影基本方法.而且,由于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!
Run Code Online (Sandbox Code Playgroud)

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

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

C tan = new D();
tan.Foo();  // which foo, which foo?
Run Code Online (Sandbox Code Playgroud)

我们搜索的继承链从跨越CD. D是有一个压倒一切的成员,所以它Foo被称为.

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

我希望这是有道理的.

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