读《你必须知道的.NET》继承本质论 Bird bird=new Chicken()

我们创建如下的三层继承层次类。

   public abstract class Animal
    {
        public abstract void ShowType(); 
    }
 
    public class Bird : Animal
    {
        private string type = "Bird";
        public override void ShowType()
        {
            Console.WriteLine("Type is {0}", type);
        }
    }
    public class Chicken : Bird
    {
        private string type = "Chicken";
        public override void ShowType()
        {
            Console.WriteLine("Type is {0}", type);
        }
    }

(1)简析对象创建过程

Bird bird=new Bird();
Bird bird创建的是一个Bird类型的引用,而new Bird()完成的是创建Bird对象,分配内存空间和初始化操作,然后将这个对象引用赋给bird变量,用示例图来表示情况就是这样:

(2)分析CLR是如何执行对象的创建过程的?

字段是如何创建的:

       我们以基类Chicken的对象创建为例,我们幻想在托管堆中,有一块叫GC Heap的空间,用于存储Chicken对象的字段,对象一经创建,CLR会找到其父类Bird,并为其字段在GCHeap分配了空间用于存储,你没听错,实例化Chicken对象也要将其基类的字段分配存储空间,然后Bird类又会继续找到其父类Animal直到System.Object,我们在各个类的字段上添加断点进行调试,Chicken chicken=new Chicken(); 创建对象的时候,会首先进到1,依次是2、3;这也证明了实例化子类也要为父类字段分配存储空间;

      所以,对象的创建过程是按照顺序完成了对整个父类及其本身字段的内存创建,并且字段的存储顺序是按照类的高低层次来的,最高层的类排在最前面,如果父类和子类出现了同名字段,则子类创建的时候,编译器会自动加与区别这是两个不同的字段,比如:type字段,Bird_type、Chicken_type 这样子,到了这一步,用示例图来表示是这样子的:

方法表:

方法表是在什么时候创建的:     

       类第一次加载到AppDomain时完成的,留意到上图中的Type_Handle没有,在对象创建时,将附加成员Type_Handle指向方法表在LoaderHeap(加载堆)上的地址,也就是先有方法表再有对象,这样就完成了对象与动态方法表的关联操作。

方法表是如何生成的?

       方法表的创建跟字段的创建过程类似,也是逐层递归知道Object类,Chicken子类生成方法列表时,先将Bird类所有虚方法复制一份,如果Chicken类有override父类的虚方法,则子类方法覆盖相应的虚方法,同时添加子类新方法,并且顺序也是父类在前,子类在后,这个顺序很重要,在后面一节方法调用的时候,就会跟这个顺序有关,也是很多面试题的考点。
总结:方法表 = 父类没有被override的方法 + 子类子类的方法,所以到了这一步,用示例图来表示应该是这样:

Q:   

      好了,问题来了,那当我们以Bird bird=new Chicken(); 创建对象的时候,bird.type 等于什么?bird.ShowType()又输出什么?本书的作者AnyTao列举了两个原则,当我们熟知这两个原则和理解字段的创建、方法表的创建,就会明白为什么会出现这样的输出结果了。
○ 关注对象原则:Bird bird=new Chicken(); 我们关注的应该是new的是什么类,也就是关注创建的是Chicken类型的对象;
○ 执行就近原则:首先访问离它创建最近的字段或者方法;
     书中关于这段输出结果的原因和结果篇章比较少,我不知道为什么AnyTao没有继续讲解下去而是作为思考抛给了读者,为此,我研究了一下,而且编写DEMO输出结果,在此我说说自己的见解,如果说得不妥请观众们评论矫正。
Bird bird=new Chicken();

bird.type的结果什么?

根据内存分布原则,此时栈上是Bird类型的变量引用,而托管堆上的是Chicken类型的对象,内存分布图应该是这样子的。

     此时托管堆上字段有两个,分别是Bird_type、Chicken_type(注意:编译器不会重新命名,此处为了便于理解),那应该输出哪个才对?根据执行就近原则,bird.type 其中的bird是Bird类型的变量引用,所以,首先会访问 Bird_type="Bird";

bird.ShowType()的结果是什么?

       我的理解是,此处也应该执行就近原则,但是Bird类中的ShowType()方法已经在Chicken类中被Override了,所以在内存分布图中的方法表上,只能找到Chicken.ShowType();而在Chicken类中, private string type = "Chicken"; type字段被赋值,所以输出结果为“Type is Chicken”

总结:

       通过上面的文字,我们可以理解继承的本质,无论把Bird.type设置为Public还是Private,父类的字段都早就已经存在子类对象所在托管堆的内存分配空间中了,只是设置为Private的时候,子类对象无法访问而已。 短短的几页书,看的时间虽短,但是整理成章花费的时间却不少,看看电脑屏幕右下角的时钟,已经花费了两个小时,包括作图、写代码验证书中结果、思考AnyTao给出的问题等,但是这一切我都觉得很值,从来没有如此的深入研究.NET底层的知识,刚毕业面试的时候回答这类问题总是瞎蒙,深入原理、庖丁解牛乃程序员立足之根本!

原文地址:https://www.cnblogs.com/waynechan/p/3570702.html