关于C#中的虚方法的问题
一直以来都对面向对象的语言如何实现虚方法和重写的这些问题很感兴趣,虽然知道最基本的一些如何使用这种设计方式,但是却一直没有找到一个详细的资料告诉我C#到底是如何做到的。
最近没什么事情就把一直想看的<<essential .net>>这本书翻了一下,让我对这件事有了一定程度了解(至少我认为是这样)。
因为这个问题会涉及到方法表,所以我认为应该先把对象如何在内存中存储的来做一个简单的介绍。下面就是一张说明对象的存储的图(完全是从书上来的),

假设我们有这样一个声明语句: Human b = new Human();
那么object reference就是我们声明的变量b, 他就是一个指针, 他指向的位置就是一个对象真正被存储的地方Object(Human)。在这块区域上主要有三个部分, 第一个部分是一个对这个对象的索引,具体的功能我还不清楚,但是他在这里没有用到,所以可以忽略。第二部分htype是一个指针, 他指向的位置存储的是这个实例所属的类的信息。 第三部分是存储这个类的数据成员。
在这里重点是关于第二个指针htype的。htype指向的区域被作者称为CORINFO_CLASS_STRUCT, 对于每一个类都有这样一个区域,而且应该是唯一的。在CORINFO_CLASS_STRUCT这块区域中,只要知道有一下三个区域就可以了:
为了配合问题的说明,我定义了两个简单的类Base和Child。Base 中的方法大部分都是虚方法, 然后在Child中也定义相同的方法,但是前面的修饰符不同。
Code这段程序在被编译成IL后, 我们可以看一下main方法是怎么样的
Code在这里我们可以注意到即使b的实例是Child类型, 在这里因为声明b是Base类型,所以在翻译成IL代码的时候,方法就直接指定的是Base.printX().
但是有一点我现在还不清楚为什么:
在Child类中,print2方法是唯一被override的一个虚方法,然后ch.print2()被翻译成了Base.print2(), 因为我原以为应该是Child.print2(), 希望哪位大虾看来这篇文章后可以给指点一下。
所以我认为,在程序执行的时候,对于非虚方法他会直接按照IL的指令去调用相应的方法,方法前指定的是哪个类型就调那个类型的方法。但是对于虚方法,在执行的时候就还有另外的做法了。
那就是当遇到虚方法时,IL中写的方法调用语句就不再起决定作用了, 取而代之的是类的实例。按照essential .net 的解释,在将一个普通法的调用翻译成IL时用的是call指令,将虚方法翻译成IL时用的是Callvirt指令(但是在上面的IL代码中可以看到即使print方法不是虚方法,对他的调用也是用的callvirt指令,也许是因为essential .net 针对的是.net 1.1 版本的缘故,现在的编译器可能已经不那么做了,这些都是我的猜测,具体原因我也不清楚)。 callvirt的指令的特殊之处就在于他会根据对象的实际类型来决定调用哪个方法,这也就是虚方法的特殊之处。
下面我们可以通过sos来调试这段程序, 通过sos我们可以看到对象和类的更具体的信息。
首先我们可以看一下Base类的方法表:
MethodDesc Table
Entry MethodDesc JIT Name
70fb6ab0 70e34944 PreJIT System.Object.ToString()
70fb6ad0 70e3494c PreJIT System.Object.Equals(System.Object)
70fb6b40 70e3497c PreJIT System.Object.GetHashCode()
71027540 70e349a0 PreJIT System.Object.Finalize()
007cc048 007c33a0 NONE FormatNumber.Base.print2()
007cc050 007c33ac NONE FormatNumber.Base.print3()
007cc058 007c33b8 NONE FormatNumber.Base.print4()
007cc038 007c3384 JIT FormatNumber.Base..ctor()
007cc040 007c3390 NONE FormatNumber.Base.print()
在这个表中我们可以看到该类的所有方法,前面的四个方法是从object继承来的四个虚方法。print2, print3, print4都是给类的虚方法,.ctor()是构造函数,print是一个非虚方法。实际上在方法表中虚方法和非虚方法就是分开存放的。Entry列是方法的入口, methoddesc 列是方法的描述, JIT列表示代码是否经JIT解释过(但是对于更细节的东西我还不清楚)。
然后是Child类的方法表(省去了前面的四个从object继承来的方法):
Entry MethodDesc JIT Name
007cc088 007c3440 NONE FormatNumber.Child.print2()
007cc050 007c33ac NONE FormatNumber.Base.print3()
007cc058 007c33b8 NONE FormatNumber.Base.print4()
007cc090 007c344c NONE FormatNumber.Child.print3()
007cc098 007c3458 NONE FormatNumber.Child.print4()
007cc078 007c3424 JIT FormatNumber.Child..ctor()
007cc080 007c3430 NONE FormatNumber.Child.print()
我们关键看print2 与 print3 和print4 的不同。首先可以看到在Child的方法表中存着在基类中定义的虚方法,但是对于非虚方法print这里并不存在。而在从Base继承来的虚方法中,print3和print4仍然存在,但是print2不存在了,这是因为我们在Child类中定义print2时在前面加了override修饰符,根据CLR的做法,如果override一个从基类继承来的虚方法,那么在这个方法表中原本应该是基类的虚方法的项就会被子类的定义给覆写调,这就是为什么print2不存在了的原因。而print3和print4, 在子类中没有用override,此时会在方法表中重新分配一项用来存储这些方法。这样一来,当程序执行到调用虚方法时(callvirt指令),他会根据对象的htype指针查找方法表,如果他发现base的该方法已经被覆写了,就会直接调用覆写后的方法,如果没有被覆写,因为在方法表里仍然存着该方法的基类的方法的入口,所以他仍然可以调用到IL指定的方法。
最近没什么事情就把一直想看的<<essential .net>>这本书翻了一下,让我对这件事有了一定程度了解(至少我认为是这样)。
因为这个问题会涉及到方法表,所以我认为应该先把对象如何在内存中存储的来做一个简单的介绍。下面就是一张说明对象的存储的图(完全是从书上来的),

假设我们有这样一个声明语句: Human b = new Human();
那么object reference就是我们声明的变量b, 他就是一个指针, 他指向的位置就是一个对象真正被存储的地方Object(Human)。在这块区域上主要有三个部分, 第一个部分是一个对这个对象的索引,具体的功能我还不清楚,但是他在这里没有用到,所以可以忽略。第二部分htype是一个指针, 他指向的位置存储的是这个实例所属的类的信息。 第三部分是存储这个类的数据成员。
在这里重点是关于第二个指针htype的。htype指向的区域被作者称为CORINFO_CLASS_STRUCT, 对于每一个类都有这样一个区域,而且应该是唯一的。在CORINFO_CLASS_STRUCT这块区域中,只要知道有一下三个区域就可以了:
- 存有指向父类的CORINFO_CLASS_STRUCT区域的指针,如果有父类的话。
- 有一个指向接口表的指针,接口表中存放的就是该类型继承的接口的htype
- 方法表
为了配合问题的说明,我定义了两个简单的类Base和Child。Base 中的方法大部分都是虚方法, 然后在Child中也定义相同的方法,但是前面的修饰符不同。
但是有一点我现在还不清楚为什么:
在Child类中,print2方法是唯一被override的一个虚方法,然后ch.print2()被翻译成了Base.print2(), 因为我原以为应该是Child.print2(), 希望哪位大虾看来这篇文章后可以给指点一下。
所以我认为,在程序执行的时候,对于非虚方法他会直接按照IL的指令去调用相应的方法,方法前指定的是哪个类型就调那个类型的方法。但是对于虚方法,在执行的时候就还有另外的做法了。
那就是当遇到虚方法时,IL中写的方法调用语句就不再起决定作用了, 取而代之的是类的实例。按照essential .net 的解释,在将一个普通法的调用翻译成IL时用的是call指令,将虚方法翻译成IL时用的是Callvirt指令(但是在上面的IL代码中可以看到即使print方法不是虚方法,对他的调用也是用的callvirt指令,也许是因为essential .net 针对的是.net 1.1 版本的缘故,现在的编译器可能已经不那么做了,这些都是我的猜测,具体原因我也不清楚)。 callvirt的指令的特殊之处就在于他会根据对象的实际类型来决定调用哪个方法,这也就是虚方法的特殊之处。
下面我们可以通过sos来调试这段程序, 通过sos我们可以看到对象和类的更具体的信息。
首先我们可以看一下Base类的方法表:
MethodDesc Table
Entry MethodDesc JIT Name
70fb6ab0 70e34944 PreJIT System.Object.ToString()
70fb6ad0 70e3494c PreJIT System.Object.Equals(System.Object)
70fb6b40 70e3497c PreJIT System.Object.GetHashCode()
71027540 70e349a0 PreJIT System.Object.Finalize()
007cc048 007c33a0 NONE FormatNumber.Base.print2()
007cc050 007c33ac NONE FormatNumber.Base.print3()
007cc058 007c33b8 NONE FormatNumber.Base.print4()
007cc038 007c3384 JIT FormatNumber.Base..ctor()
007cc040 007c3390 NONE FormatNumber.Base.print()
在这个表中我们可以看到该类的所有方法,前面的四个方法是从object继承来的四个虚方法。print2, print3, print4都是给类的虚方法,.ctor()是构造函数,print是一个非虚方法。实际上在方法表中虚方法和非虚方法就是分开存放的。Entry列是方法的入口, methoddesc 列是方法的描述, JIT列表示代码是否经JIT解释过(但是对于更细节的东西我还不清楚)。
然后是Child类的方法表(省去了前面的四个从object继承来的方法):
Entry MethodDesc JIT Name
007cc088 007c3440 NONE FormatNumber.Child.print2()
007cc050 007c33ac NONE FormatNumber.Base.print3()
007cc058 007c33b8 NONE FormatNumber.Base.print4()
007cc090 007c344c NONE FormatNumber.Child.print3()
007cc098 007c3458 NONE FormatNumber.Child.print4()
007cc078 007c3424 JIT FormatNumber.Child..ctor()
007cc080 007c3430 NONE FormatNumber.Child.print()
我们关键看print2 与 print3 和print4 的不同。首先可以看到在Child的方法表中存着在基类中定义的虚方法,但是对于非虚方法print这里并不存在。而在从Base继承来的虚方法中,print3和print4仍然存在,但是print2不存在了,这是因为我们在Child类中定义print2时在前面加了override修饰符,根据CLR的做法,如果override一个从基类继承来的虚方法,那么在这个方法表中原本应该是基类的虚方法的项就会被子类的定义给覆写调,这就是为什么print2不存在了的原因。而print3和print4, 在子类中没有用override,此时会在方法表中重新分配一项用来存储这些方法。这样一来,当程序执行到调用虚方法时(callvirt指令),他会根据对象的htype指针查找方法表,如果他发现base的该方法已经被覆写了,就会直接调用覆写后的方法,如果没有被覆写,因为在方法表里仍然存着该方法的基类的方法的入口,所以他仍然可以调用到IL指定的方法。




}
}
浙公网安备 33010602011771号