此篇博文是我最近看clr via c#的一些体会,可能有不对的地方,欢迎指正。
我们写c#代码,将方法定义在一个类中,然后编译代码再执行,执行的时候,方法也好,字段也好,不管是实例方法/字段还是静态方法/字段,一定会在内存中的某处占用一定的内存空间。不管要访问某个字段,还是要调用某个方法,都必须知道字段或者方法在内存中的位置,这篇文章主要讨论的是clr如何获取到要调用方法在内存中的位置,从而进行方法调用的。
一、类型对象
首先我们先引入一个概念:类型对象,“类型对象”这个词并不是.net中的术语,只是我给它起的名字,所谓类型对象,是描述一个对象的类型的对象,这有点拗口,首先明确一点,类型对象也是一个对象,与咱们平时new object()得到的对象一样,在托管堆里分配内存,这个对象的作用是描述一个对象的类型。
平时我们再写代码的时候,如果要查看某个对象的类型是什么,就会调用这个对象的GetType方法,来获取了一个Type类的子类(因为Type类是抽象类,所以GetType返回的一定是其子类),GetType方法返回的就是某个对象的类型对象。
类型对象也是某个类型(Type类的子类)的实例,所以它会存在于托管堆中,类型对象并不是由程序员来实例化的,它的实例化由clr进行。在我们写代码的时候,必然会用到各种各样的类型,常用的int,string,bool,object,或者我们自己定义的结构体,类等,在程序运行时,clr会在第一次使用某个类型之前(比如实例化某个对象,或者调用某个类型的静态方法,静态字段等),实例化一个该类型的类型对象,并且该对象会常驻于内存中,直到所属的应用程序域(App Domain)被卸载,并且对于同一个类型,只实例化一个类型对象。想想也知道,因为某个类型的所有实例的类型都是一样的,所以只有一个类型对象是合理的。
二、方法在内存中的位置在哪里
在.net中,实例化一个对象(不管是值类型还是引用类型)的时候,只会为这个对象实例字段分配内存空间,不会为这个对象中定义的方法分配内存,实际上类型的所有实例共享相同的方法代码,对一个类型的不同实例而言,他们只是实例字段不相同。在程序运行时,对于每一个类型来说,该类型中定义的方法存在于内存中的某个位置,该类型的任意一个实例要调用某个方法的时候,有一种机制可以找到这个位置,从而执行该方法的代码。
对于引用类型,在实例化的时候,会在堆上分配内存,除了为该对象的所有实例字段分配内存空间以外(一个类型的静态字段也是所有实例共享的,与方法相似),还会分配一些额外的空间,这些额外的空间用来存储对象的类型对象指针(Type object pointer)和同步块索引(sync block index )。对于值类型来说,实例化的时候不会有这两个额外的对象,但是当值类型被装箱的时候,会加上这两个额外对象。同步块索引是用于线程同步的,本文不做讨论,对象实例化以后再堆上的内存分配情况如下图。
在这里我们说一下类型对象指针,从名字可以看出来类型对象指针是一个指针(这简直就是废话,如果不知道指针为何物,可以理解为引用),既然是一个指针,那就必然指向了内存中的某个位置,对于某个类型的所有实例来说,这些实例对象的类型对象指针都指向内存中的同一个位置,类型对象指针指向的这个位置,其实是另外一个类的实例,不过这个实例对象比较特殊,它是Type类的一个子类。在我们平时写代码的时候通过调用从object继承来的GetType()方法可以得到这个对象,对于值类型在调用GetType方法的时候,不是直接调用,会先对这个值类型实例进行装箱,然后再调用GetType方法,通过类型对象指针,可以获取某个对象的类型对象。在这个类型对象中保存了类型定义的所有静态字段,以及一个方法表,对于类型中定义的所有方法,不管是静态方法,实例非虚方法还是实例虚方法,都在方法表中有一条记录,这条记录包含了方法在内存中的位置信息,通过方法表,clr可以知道运行时某个类型的某个方法在内存中的位置,从而可以让clr正确的调用方法。
在这里有一点比较有趣的是,本文一开始就提到了,类型对象也是一个对象,与咱们实例化出来的其它对象一样,存在于托管堆中,那么它也拥有类型对象指针和同步块索引,它也有它自己的类型对象,所有类型对象的类型都是System.RuntimeType,所有的”类型对象“的“类型对象指针“都指向了内存中的同一个位置,不妨我们叫它根类型对象,根类型对象的类型对象指针是指向它自己的。就好比根类型对象就是.net中的上帝,它指定了一批人做教皇,并且赋予这些教皇权力来描述普通人的身份,一个普通人是男是女,家里几口人,人均几亩地,地里几头牛,都是教皇来描述的,并且同一类型的普通人由唯一的一个教皇来管理,而教皇的身份是由上帝来描述的,至于上帝,它自己说它是上帝,所以它就是上帝。
为了后面便于说明,我们定义如下两个类:
class BaseClass { internal void SayHello() { Console.WriteLine("BaseClass say hello!"); } internal virtual void PlayDota() { Console.WriteLine("BaseClass play dota,用的英雄是沙王"); } } class DrivedClass:BaseClass { internal new void SayHello() { Console.WriteLine("DirvedClass say hello"); } internal override void PlayDota() { Console.WriteLine("DrivedClass play dota,使用的英雄是幻影刺客"); } }
现在我们来实例化两个个BaseClass类的实例和两个DrivedClass类的实例
BaseClass baseClass1=new BaseClass();
BaseClass baseClass2=new BaseClass();
DrivedClass drivedClass1=new DrivedClass();
DrivedClass drivedClass2=new DrivedClass();
对于这四个对象及其他们类型对象在内存中的情况如下图。
为了验证前面说的上帝教皇普通人的问题,可通过以下代码验证:
BaseClass baseClass1=new BaseClass();
BaseClass baseClass2=new BaseClass();
DrivedClass drivedClass1=new DrivedClass();
DrivedClass drivedClass2=new DrivedClass();
object o=new object();
string s = "aaaaa";
Type baseClass1Type = baseClass1.GetType();
Type baseClass2Type = baseClass2.GetType();
Type drivedClass1Type = drivedClass1.GetType();
Type drivedClass2Type = drivedClass2.GetType();
Type objectType = o.GetType();
Type stringType = s.GetType();
Console.WriteLine("baseClass1引用的对象类型为"+baseClass1Type.ToString());
Console.WriteLine("drivedClass1引用的对象类型为" + drivedClass1Type.ToString());
Console.WriteLine("baseClass2引用的对象类型为" + baseClass2Type.ToString());
Console.WriteLine("drivedClass2引用的对象类型为" + drivedClass2Type.ToString());
Console.WriteLine("o引用的对象类型为" + objectType.ToString());
Console.WriteLine("s引用的对象类型为" + stringType.ToString());
if (object.ReferenceEquals(baseClass1Type,baseClass2Type))
{
Console.WriteLine("baseClass1和baseClass2两个普通人是由同一个教皇描述的");
}
if (object.ReferenceEquals(drivedClass1Type,drivedClass2Type))
{
Console.WriteLine("drivedClass1和drivedClass2两个普通人是由同一个教皇描述的");
}
if (!object.ReferenceEquals(baseClass1Type,drivedClass1Type))
{
Console.WriteLine("baseClass1和drivedClass1两个普通人是由不同的教皇描述的");
}
Type baseClassTypeType = baseClass1Type.GetType();//获取类型BaseClass的类型对象的类型对象
Type drivedClassTypeType = drivedClass1Type.GetType();//获取类型DrivedClass的类型对象的类型对象
Type objectTypeType =objectType.GetType();//获取类型Object的类型对象的类型
Type stringTypeType =stringType.GetType();//获取类型string的类型对象的类型
Console.WriteLine("BaseClass的类型对象的类型是"+baseClassTypeType.ToString());
Console.WriteLine("DrivedClass的类型对象的类型是"+drivedClassTypeType.ToString());
Console.WriteLine("object的类型对象的类型对象是"+objectTypeType.ToString());
Console.WriteLine("string的类型对象的类型对象是"+stringTypeType.ToString());
if (object.ReferenceEquals(baseClassTypeType,drivedClassTypeType))
{
Console.WriteLine("管理BaseClass的教皇和管理DrivedClass的教皇都是归上帝管的");
}
if (object.ReferenceEquals(baseClassTypeType,objectTypeType))
{
Console.WriteLine("管理BaseClass的教皇和管理object的教皇都是归上帝管的");
}
if (object.ReferenceEquals(objectTypeType,stringTypeType))
{
Console.WriteLine("管理object的教皇和管理string的教皇都是归上帝管的");
}
if (object.ReferenceEquals(objectTypeType,objectTypeType.GetType()))
{
Console.WriteLine("上帝自己管自己,它说自己是上帝它就是");
}
控制台输出如下
讲到这里,我们大致明白了clr要调用一个方法的时候,需要先找到该方法所在的类型对象,然后从该类型对象的方法表中找到该方法在内存中的位置进行调用。但是c#作为一门面向对象的编程语言,其中有一些复杂的情况,具体的情况包括两种:方法重写(override)和方法隐藏(new)。对于这部分内容在下一篇博文中进行讨论。