深入了解.NET中继承和多态(下)

 

很久都没写BLGO了,关于多态的第3篇文章一晃就1年了才写。有时比较迷茫,感觉太多东西都要学,什么都想学,却找不清方向了。呵呵,看着好多牛人的BLOG觉得自己水平实在是太差了。呵呵。有时甚至觉得自己写的东西太低级了。呵呵,或许是自己抱怨太多了,还是静下心来慢慢学习吧。以后一定多写一些东西,自己经常看看还是挺有帮助的。

 


 

 

 

原创   深入了解.NET中继承和多态(上)

 

原创  深入了解.NET中继承和多态(中)

 

如果大家对多态的机制还不了解,可以先查看上面这2篇文章。本篇本打算使用一些例子说话,但是实际大家明白了方法表的布局结构。其实是根本不需要任何实例去讲解了。所以这一篇主要算是查缺补漏吧。

 

一 多态的例子

 

这个例子,主要是为了解释上一篇中遗留的new关键字时候的问题。我们主要从两个方面来讲解这个列子:

 

1:变量类型同对象类型不同的场合

C2和C3_1这两个对象的变量类型都是CPU类型,分配在栈上:

在编译前:他们都只能使用变量类型所具有的方法,也就是CPU::FUN() 方法。

在编译时:编译器发现CPU::FUN()是一个虚方法,然后便获得了FUN的方法槽偏移量为【28H】 ,C2和C3_1对象调用Fun()方法的地址是【对象方法表地址】+【28H】

在运行时:因为虚方法是通过callvirt 指令调用 ,需要知道具体的对象类型,这个时候C2对象是IntelCpu类型,而C3_1对象是NewCpu类型。于是访问C2.FUN()时地址是【IntelCpu类型地址】+【28H】;而C3_1.FUN()的地址是【NewCpu】+【28H】

 

上面就是编译后的内存方法表的布局情况。IntelCpu类型使用了override关键字,所以方法槽偏移量为【28H】不再指向CPU对象的方法地址,而NewCpu类型对象使用了new关键字,所以继承的方法槽偏移量为【28H】的地址仍旧指向CPU对象的方法地址,只是在下一个方法槽创建了一个新的fun方法。更具上面的图就很清楚的看出了运行时的结果。

 

 

 

2 变量类型同对象类型相同的场合

C1和C3_2这2个对象的变量类型与类型对象是相同的。

在编译前:C1变量是Cpu类型,所以能见的是CPU::FUN()方法;而C3_2变量是NewCpu类型,能见的是NewCpu::Fun()方法(因为用New关键字覆盖了)

在编译时:发现C1的fun方法是虚方法,所以才C1.fun访问地址是【对象方法表地址】+【28H】 ;而C3_2的fun方法不是虚方法,所以编译器可以直接确定此方法的地址【0x0001】。

在运行时:同样是使用callvirt 指令调用,因为他们变量类型与类型对象是相同的,所以不会表现出多态。

上面就是这种情况的方法表布局。可以看到NewCpu类型对象有两个fun方法,一个是继承于Cpu类型对象的虚方法,一个是自己新建的方法。C3_1对象同C3_2对象区别就在于他们栈上的变量类型不同。C3_1在编译时是Cpu对象,可见的fun是虚方法,所以获得了继承的Fun方法的方法槽偏移量,而C3_2在编译时NewCpu对象,可见的fun方法是非虚方法,所以直接得到了自己的fun方法的地址。

 

 

另外要补充的就是对于new override的情况,这个是和new一样的,不同的只是自己新建的这个方法是一个虚方法。而如果直接使用override方法,被重写的方法仍旧是虚方法,可以被自己的子类继续重写,一层一层。

 

二 更进一步的例子

对于override和new的情况,应该说是应该比较清楚了,那么接着看下面的列子吧。

上面的列子中在父类中有2个虚方法,一个非虚方法。而子类中,只是覆盖了一个虚方法。而我们要关注的也就是这个子类的调用情况。因为父类没有任何方法被重写,所以准确的说,这里并不能算是一个多态的例子。但是有了虚方法,有了new,总是容易和多态混淆。还是那句话,弄清楚了方法表布局,一切都不在是问题。

 

 

再次提醒一次,NewCpu方法表只会继承基类的虚方法到自己的方法槽表中,并且保持相同的布局; 所以此时的内存方法表应该如上图所示。fun1和fun3两个虚方法继承于父类,并且保持了相同的布局。而fun2是非虚方法,所以没有被继承。

 

调用fun1方法,和前一个列子是相同的,因为被覆盖,并且是非虚方法,所以编译时确定了地址,

调用fun2方法,因为fun2是父类的一个非虚方法,所以也是编译时确定了地址

调用fun3方法,因为fun3方法是父类的一个虚方法,所以编译时只能确定方法槽的偏移量,而要在运行时确定运行地址。

 

上面是编译后的汇编代码,大家也可以到call指令后的地址形式。只有fun3是间接寻址,而其他是直接寻址。这里唯一的问题就是对fun2方法的调用。fun2没有被继承下来,那么NewCpu对象是如何去Cpu对象中得到他的地址的呢?

 

 

上面是NewCpu生成的IL代码,我们可以发现Extends项目中只是了它的父类是Cpu类,这样在编译时,虽然在自身类中找不到fun2方法,但是系统会去他的父类中找到此方法并确定方法的地址,而在我们编译前,智能感知中能找到fun2,也是依靠元数据来实现的。而在子类实例化之前,调用父类构造函数,应该也是同一个道理。

 

三 终极武器

光凭空YY,是解决不了问题的,这个时候我就要要用sos.dll来调试代码; 看看内存布局到底是个啥样子。

为了看的清楚,在上面代码中加入了Cpu的对象c,来分别调用3个方法:

 

1:查看实际的内存方法表布局

 

 

从上面我们清楚的看到Cpu类型对象的方法表中有我们自己定义的3个方法,Entry就是方法槽偏移量,也是递增的。接下来看看NewCpu对象的方法表:

如何,看明白了吧,继承下来的fun1和fun3的地址是一样的,而方法表中确实没有fun2的身影。平时我们说的继承,子类会继承父类的所有方法(包括私有方法,只是不能访问,但不包括构造方法),这个实际是逻辑上的继承,而真正物理上的集成只对虚方法有效。而且对于非虚方法也不需要去继承到子类中,因为这就是代码重用吗。哈哈!

 

2:MethodDesc

在看看上面的MethodDesc这个字段,方法描述(MethodDesc)是CLR知道的方法实现的一个封装。方法描述在类加载过程中产生,初始化为指向IL。每个方法描述带有一个预编译代理(PreJitStub),负责触发JIT编译。下图显示了一个典型的布局,方法表的槽实际上指向代理,而不是实际的方法描述数据结构。对于实际的方法描述,这是-5字节的偏移,是每个方法的8个附加字节的一部分。这5个字节包含了调用预编译代理程序的指令。5字节的偏移可以从SOS的DumpMT输出从看到,因为方法描述总是方法槽表指向的位置后面的5个字节。在第一次调用时,会调用JIT编译程序。在编译完成后,包含调用指令的5个字节会被跳转到JIT编译后的x86代码的无条件跳转指令覆盖。(转)

 

 

 

3:EEClass

EEClass在方法表创建前开始生存,它和方法表结合起来,是类型声明的CLR版本。实际上,EEClass和方法表逻辑上是一个数据结构(它们一起表示一个类型),只不过因为使用频度的不同而被分开。经常使用的域放在方法表,而不经常使用的域在EEClass中。这样,需要被JIT编译函数使用的信息(如名字,域和偏移)在EEClass中,但是运行时需要的信息(如虚表槽和GC信息)在方法表中。

 

对每一个类型会加载一个EEClass到应用程序域中,包括接口,类,抽象类,数组和结构。每个EEClass是一个被执行引擎跟踪的树的节点。CLR使用这个网络在EEClass结构中浏览,其目的包括类加载,方法表布局,类型验证和类型转换。EEClass的子-父关系基于继承层次建立,而父-子关系基于接口层次和类加载顺序的结合。在执行托管代码的过程中,新的EEClass节点被加入,节点的关系被补充,新的关系被建立。在网络中,相邻的EEClass还有一个水平的关系。EEClass有三个域用于管理被加载类型的节点关系:父类(Parent Class),相邻链(sibling chain)和子链(children chain)。

 

我们通过使用!DumpClass可以查看EEClass

 

上面的代码中我们从NewCpu对象开始查看,可以看到她Parent Class的地址,这个就是当前父对象的类型EEClass地址。我们继续看Cpu对象的父对象发现是Object,而她的父对象地址是 00000000。而这个结构中,还包括了类中定义的静态字段和实例字段数;Vtable Slots是虚方法数量和实现的接口方法,而父类的借口方法也会被继承下来,因为接口方法也是虚方法,虽然被继承但不能被重写,因为IL代码中有final关键字 ,而Total Method  Slots是类中总的的方法。由此可见,在NewCpu方法中,虚方法有6个,4个继承与Object,2个继承与Cpu;而总的方法有7个,6个虚方法,和一个继承与Cup的非虚方法。

 

 

关于使用sos调试的命令可以参见:http://msdn.microsoft.com/zh-cn/library/bb190764.aspx

 

 

四 抽象方法、虚方法和接口方法

 

上面的列子包含了抽象方法,虚方法和接口方法,以及他们的继承和重写。实际上抽象方法和接口方法都是虚方法,只不过他们不需要也不能显示的使用virtual关键字。我们通过ILDASM来查看他们的IL有什么区别。

 

可以看到3种方法的IL代码都有virtual关键字,说明他们全是虚方法。不同的是接口和抽象方法都有abstract方法,表示他们都是抽象的,所以非抽象类或非接口继承他们之后都需要被实现。

 

我们接着看继承他们的类的IL代码

 

上面的Cpu类分别重写了3种方法。抽象方法和虚方法是相同的,而接口却多了一个final关键字,这样的话,此接口方法不能被子类重写。虽然他是虚方法。如果需要接口方法能被重写,需要显示的加上Virtual关键字。而如果希望一个虚方法不能被不能被子类重写,那么可以使用sealed关键字,而不能使用private来限制虚方法。 效果如下IL代码:

 

有意思的是,如果你吧虚方法定义为private,在编码时,只能感知会更具元数据来显示出这个方法为可重写的方法,但是编译时会报错,所以不知道这算不算一个小BUG。但是在C++中,私有虚函数是有意义的,http://topic.csdn.net/t/20040805/16/3245820.html

 

 

 

五 总结

.NET中的继承和多态的第3篇文章终于写完了。其实自己也是从对多态懵懵懂懂的认识开始的,在网上看了好多介绍继承和多态,但很多都是给你一些自己总结的规则,看的人云里雾里,有一些也介绍到了方法表,内存结构,但是介绍的都很浅,所以自己打算稍微深入研究一下。结果一直没写下来。感觉对于继承和多态的把握关键还是在内存模型。内存结构了解了,万变不离其中。在复杂的情况也能分析的清楚。但是鉴于本人能力有限,对于内存模型那块,也是知之甚少,难免有错误的地方。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值