由于某些媒体和个人喜欢拿来主义,所以笔者加了水印,见谅见谅。。。不影响大家看就是了,字比较小,可将浏览器放大。
今天先来说说.net中的类型,我们先不去区分什么值类型在声明它的地方,引用类型类型的实例在GC堆中.实际在CLR中有3个堆(GC堆,Load堆,大对象堆),我们今天要描述的是Load堆用来存放类型而不是类型的实例,关于GC堆是后面要做的事情.只说类型,首先看一下笔者定义的类型.
3
4 public struct TestValueType1
5 { }
6
7 public interface ITestInterface1
8 { }
9
10 public delegate void TestDelegate1();
在这里我只定义了类型,类型中没有任何成员,下面我将编译生成程序集,然后来看看CIL中他们是什么样子.
首先来看看引用类型TestObjectType1
3 TypDefName: TestDemo1.Type.TestObjectType1 ( 02000003 )
4 Flags : [ Public ] [AutoLayout]//类型字段加载方式 [ Class ] [AnsiClass] [ BeforeFieldInit ]
5 Extends : 01000001 [TypeRef] System.Object //父类
6
7 TypeRef # 1 ( 01000001 )
8 -------------------------------------------------------
9 Token: 0x01000001
10 ResolutionScope: 0x23000001
11 TypeRefName: System.Object
可以看到在CIL的元数据部分,可以清晰地看到TestObjectType1继承自Object以及它的类型内存布局,这是CLR用来区分如何分配实例的内存的关键.还有类型标记(反射和JIT编译时会使用类型标记来快速的查找类型).
下面来看看Struct和Class有什么不同?
2 -------------------------------------------------------
3 TypDefName: TestDemo1.Type.TestValueType1 ( 02000004 )
4 Flags : [ Public ] [SequentialLayout] //类型字段加载方式 [ Class ] [Sealed] [AnsiClass] [ BeforeFieldInit ]
5 Extends : 01000002 [TypeRef] System.ValueType
6 Layout : Packing: 0 , Size: 1
7
8 TypeRef # 2 ( 01000002 )
9 -------------------------------------------------------
10 Token: 0x01000002
11 ResolutionScope: 0x23000001
值类型都继承自ValueType,当然ValueType也最终继承自Object,在这里大家可以看到它的Flags中也是Class,而且还是Sealed.还有一个需要注意的是Layout中,它的Size竟然是1,我们并没有定义任何的成员阿?这里的1笔者将会在后面给出答案.
我们再来看看接口有什么不同
2 -------------------------------------------------------
3 TypDefName: TestDemo1.Type.ITestInterface1 ( 02000005 )
4 Flags : [ Public] [AutoLayout] [Interface] [Abstract] [AnsiClass]
可以看到接口不存在任何父类,它只是一个纯抽象的契约而已,很多书上都说任何类型都最终继承自Object是不严谨的.那么为什么接口不继承自Object呢,简单来说由于Object已实现的方法,如果接口继承自它,那么接口也就有了这些虚方法和受保护的方法,虚方法可以重写,那么就破坏了接口做为契约的本质.在实现了接口的所有类型的方法表中,将开辟两个Tostring,两个GetHashcode等的方法槽造成你调用方法时产生二义性,并且还会大量的占用内存(虽然一个方法槽只有4个字节,但是架不住类型多).你或许会说两个方法槽最终可以指向同一个方法地址,Ok.这样说两个相同的方法有同一种实现,即占用了内存,还什么用都没有,为什么要这样做呢?
OK,讨论完接口我们来看看委托,这里我们只是为了解一下委托的类型,关于委托笔者会单独写一篇文章来探索它.
2 -------------------------------------------------------
3 TypDefName: TestDemo1.Type.TestDelegate1 ( 02000006 )
4 Flags : [ Public ] [AutoLayout] [ Class ] [Sealed] [AnsiClass]
5 Extends : 01000003 [TypeRef] System.MulticastDelegate //父类
委托在实际就是一个类,继承自(MulticastDelegate,它又继承自Delegate,而最终也是Object的孩子),并且和值类型一样都是Sealed.表示它们不可以被继承,至于为什么不可以被继承,笔者将会在后续的文章中介绍. 我们在这里讨论了Class,Interface,struct,Delegete 发现他们的类型标记都是0200000X,都是用TypeDef来定义类型,用TypeRef来表述父类,并且有足够的信息来描述你写的C#代码.而且除了Interface,全部都被定义为Class.那么当CLR来加载这些元数据时,靠什么来区分你所定义的Struct和Class呢?CLR如何快速去查找类型呢?
dll中的数据到底是什么样子的,当CLR加载一个DLL乃至加载一个类型到物理内存的时候如何查找类型?
在编译好的dll中,实际上是由很多张表,以及一些特定的string堆,Guid堆,BLOB堆来组成的元数据,我们先来看看TypeDef表.你所定义的类型都被放在TypeDef表中,这个表你可以想象成如同二维的数据库表一样,有Token类型标记码作为主键,还可以查找到它的父类(Extends)的类型标记码.以及类型到底是Class,Valuetype,Interface(图中Flags的注释中的C,I,V)前两个是根据他们的父类来识别,而Interface有关键字来表示,参照上面的元数据.
我们现在来看看CLR如何加载一个类型.
2
3 TestValueType1 val = new TestValueType1();
相关IL
IL_0001: newobj instance void TestDemo1.Type.TestObjectType1/*02000003*/::.ctor()
IL_0010: initobj TestDemo1.Type.TestValueType1/*02000004*/
原来在IL代码中是使用类型标记码来标记语句,然后CLR通过标记加载它的.我们使用SOS来调试一下,看看在CLR在内存中把类型表现为什么样子.
TestObjectType1 obj = new TestObjectType1(); //调试C#代码的当前语句
TestValueType1 val = new TestValueType1();
0022eb9c 0037009e TestDemo1.Type.TestMain.Main()
LOCALS://线程栈地址 0x0022eba8 = 0x00000000//GC堆地址(引用类型) or 线程栈地址(值类型)
//可以看到引用类型也被初始化为0,但它是一个地址,地址为0也就是引用不存在,为null
0x0022eba4 = 0x00000000 //这里可以看到值类型被默认初始化为0
TestValueType1 val = new TestValueType1(); //调试C#代码的当前语句
LOCALS:
0x0022eba8 = 0x0240b928
在new执行后,obj的内存在GC中被分配,可以看出new关键字分配了内存,并将分配好的内存地址返回给栈上的地址空间。现在我们来看看0x0240b928这块内存空间有什么?
!dumpobj 0x0240b928
MethodTable: 001538f4 // 方法表
EEClass: 00151564 // 类型关系图
Fields: none // 字段
可以看到在在这块地址上有方法表的地址,字段列表的地址,以及类型继承关系的地址。让我们一个一个来看,首先EEclass
Class Name: TestDemo1.Type.TestObjectType1
mdToken: ed13f792 0 2000003 //红色部分是Dll被加载到虚拟内存空间的地址
Parent Class : 6c1a3ef8 // 父类的地址
Method Table: 001538f4 // 方法表的地址,与上面的指向是一样的
Vtable Slots: 4 // 4个虚方法
Total MethodSlots:5//方法表中的方法个数
ClassAttributes:100001//类型的类别 (引用类型)
NumInstanceFields:0//实例字段的个数
NumStaticFields: 0 //静态字段的个数
MdToken是记录了类型在元数据表里(IL编译后的类型标记码)的标记。这样我们在使用反射时,就可以很快地定位到元数据的信息,前面的那一部分(ed13f792)是dll在虚拟内存中的地址.
例如 System.Type t = typeof(TestObjectType1); 将会定位到类型的mdToken,然后找到类型所在Dll中的元数据表Typedef,根据2000003这个主键找到元数据,读取并返回一个Type类型的实例 t,到当前线程栈的内存空间中。
接下来我们看看父类的内容,ParentClass
ClassName: System.Object
//红色部分是Dll被加载到虚拟内存空间的地址mdToken: f5dcf17a 02000002 //注意TypeRef的Token,并不是Type的真实Token,这里是Object在MSCorlib.dll中的真实标记。
Parent Class: 00000000//没有父类
MethodTable: 6c4bf5e8 //方法表
Vtable Slots:4//4个虚方法
Total MethodSlots: a //总计10个方法
ClassAttributes:102001
NumInstanceFields:0//无任何字段
NumStaticFields:0
Console.Read();
LOCALS://当前线程栈地址
0x002cf1c4 = 0x00000000 // 由于结构没有定义任何字段所以默认为0
在这里我们没有定义任何字段,上面的元数据中我们看到了值类型不定义任何字段,它的大小也为1,因为值类型不可能为Null,所以它不可能没有大小,也就是说值类型在栈上只要分配了,就一定要有值.
关于值类型和引用类型在栈上的状态,我们以后会继续分析,这里为了简化仅仅是点到为止.
这里为了可以看到值类型实例的类型在内存中的状态,笔者只有将它装箱后才能根据它栈中指向GC堆的地址来拿到它类型的地址,具体原因我们在以后介绍.
object obj = val; //将值类型装箱
Console.Read(); //调试当前点
Fields: None
这里可以看到装箱后我们找到了实例(这时候是一个引用了)的地址,并且跟踪到了类型.我们来看看它的EEClass是怎样的?
Class Name: TestDemo1.Type.TestValueType1
mdToken: d6f0251 02000004 //与元数据中的类型标识码一样
Parent Class : 6b908a10
Module: 00312e9c
Method Table: 00313894
Vtable Slots: 4
Total Method Slots: 4
Class Attributes: 100109 //代表是值类型的布局方式
NumInstanceFields: 0
NumStaticFields: 0
与引用类型相比只有类型布局不一样,而且方法表的总数不一样,这是因为值类型编译器不会生成默认构造函数,而我们上面可以用new完全是c#给我们的一种语法糖而已。
我们来看看它的基类吧。
Class Name: System.ValueType
mdToken: 9590d7902000009
Parent Class : 6c1a3ef8 //都是Object
Method Table: 6bbcf730
Vtable Slots: 4
Total Method Slots: 5
Class Attributes: 102081 Abstract, 不可被实例化
NumInstanceFields: 0
NumStaticFields: 0
它的父类是ValueType,那么Valuetype的父类呢, 和上面TestObjectType1的ParentClass地址一样。说明了什么呢? 仔细看这里你会发现,他总共有5个方法,其中4个是虚的,那是从Object继承下来的,但是它的子类TestValueType1却只有4个方法。为什么?因为ValueType有一个受保护的构造函数,而构造函数是不继承的,那么为什么是受保护的?因为当你定义抽象类后,会在编译时默认的生成受保护的构造函数,自ValueType以后,它的所有子类都表现为了值类型的特性,没有默认构造函数,在声明的地方初始化,即可能在栈中,也可能在GC堆中。
总结:
当代码编译为dll时,每一个类型在元数据的Typedef表中,会分配一个MdToken(类型标记),当你写的方法需要访问这个类型时,也是使用MdToken到相关Dll的元数据表去加载它到Load Heap,LoadHeap是用来存放类型的空间,它并不保存类型的实例.当clr加载完类型后,就会根据你的代码初始化值类型或者引用类型,clr会根据元数据表中父类类型object,valuetype来区分是引用类型,值类型到相应的地址空间(GC堆或线程栈) ,实例化这部分内容,我们在后续的文章中继续讨论.在C#的类型中,我们可以定义字段,属性,方法,事件和嵌套类,但我们跟踪类型的EEclass,发现类型中只有两类成员,字段(事件就是一个委托,而委托只是一个类型,所以事件就是一个字段而已,但表现有些特殊后续介绍)和方法(属性实际就是方法).
在后续的文章中我们将陆续的研究字段,方法,然后再回过头来谈论类型和类型的实例(Gc堆和栈),反射以及垃圾回收器,异常管理等内容.