代码改变世界

【读书笔记】.NET本质论第四章-Programming with Type(Part One)

2009-07-30 10:40  横刀天笑  阅读(1237)  评论(6编辑  收藏  举报

在上一章中主要探讨的是CTS中的类型,基本上是类型的“静态结构”,本章将主要涉及类型的运行时结构。你定义了一个类型,然后实例化它,那么它在内存中的布局到底是什么样子的呢?声明一个类型到底占多少内存?是分配在栈上还是堆上?这些都是本章需要讨论的话题。不过这一篇先说一些简单的问题。

一个类型的实例要么是一个对象,要么是一个值,这要看这个类型是如何定义的。一般来讲,自定义的类的实例都是对象,而所有直接或间接从System.ValueType类派生的类型却是值类型(在这里,请区分类与类型的区别)。

好,那我这样是不是可以定义一个值类型了呢:

   1: public class MyValueType : ValueType
   2: {
   3:     //some code
   4: }

嗯,这样竟然是不行的,C#编译器将会给出如下的错误(而且你发现没,在Vs里输入上面的代码时,输入ValueType的时候却没有智能感知,也没有代码着色):

'MyValueType' cannot derive from special class 'System.ValueType'

哦,原来把System.ValueType当做一个特殊类处理了。

既然不能从System.ValueType继承,那我们有什么办法定义值类型啊:

   1: public struct MyValueType
   2: {
   3:  
   4: }

就这样,你的MyValueType就是一个值类型了,使用ILDasm看看:

.class public sequential ansi sealed beforefieldinit MyValueType
extends [mscorlib]System.ValueType
{
}
延伸阅读
也许你比较了上面的IL代码与一个普通的用class定义的类型的区别,除了MyValueType从System.ValueType继承,而通常定义的一个class从System.Object外,我们还发现一个sequential元数据,而在class的对应位置应该是一个auto元数据。这是什么意思呢?
实际上这属于CLR控制类型中字段布局的问题,你写一个类型,在写代码的时候,字段的排列肯定是有顺序的,那么CLR如何安排这些字段呢?有三种布局模式,实际上也是LayoutKind枚举的三个成员:
   1: [Serializable, ComVisible(true)]
   2: public enum LayoutKind
   3: {
   4:     //CLR自动控制内存布局
   5:     Auto = 3,
   6:     //使用偏移量在代码中显式控制布局
   7:     Explicit = 2,
   8:     //按照开发人员书写代码时的字段顺序控制
   9:     Sequential = 0
  10: }
如果使用class定义一个类型,C#编译器默认会使用LayoutKind.Auto,而如果定义一个struct,C#编译器默认会使用LayoutKind.Sequential。因为.NET中的struct的存在实际上是为了与哪些非托管代码交互的,这个时候就必须知道你的内存布局是个啥样子的,如果你使用Auto这种方式,那布局就是由CLR自动控制,你不知道CLR到底是如何自动的,也就无法交互了。不过,如果你确定你定义的这个struct不会与非托管代码交互,你也可以使用如下这样的代码覆盖C#编译器的默认设置了:
   1: [StructLayout(LayoutKind.Sequential)]
   2: public class MyObject
   3: { 
   4:  
   5: }
   6: [StructLayout(LayoutKind.Auto)]
   7: public struct MyValueType
   8: {
   9: }
下面是对应的IL代码:
   1: .class public sequential ansi beforefieldinit MyObject
   2:     extends [mscorlib]System.Object
   3: {
   4: }
   5:  
   6: .class public auto ansi sealed beforefieldinit MyValueType
   7:     extends [mscorlib]System.ValueType
   8: {
   9: }
现在换了个儿,class的使用sequential,而struct使用auto(注意,LayoutKind和StructLayout都来自System.Runtime.InteropServices命名空间,请添加对应的using指令)。
CLR还允许你使用LayoutKind.Explicit配合FieldOffsetAttribute显式的指定每个字段的偏移(不过一般请不要这样做)。代码如下:
   1: [StructLayout(LayoutKind.Explicit)]
   2: public struct MyValueType
   3: {
   4:     [FieldOffset(0)]
   5:     public int A;
   6:  
   7:     [FieldOffset(4)]
   8:     public int B;
   9: }
关于这个的更多示例可以参见MSDN

原来这个struct也是用.class元数据描述的,它还继承了System.ValueType。不过这个MyValueType却已经加上了sealed,这样你就不能再从MyValueType派生了。还有没有其他方法定义值类型呢?有,那就是枚举:

   1: public enum Color
   2: { 
   3:     Red,
   4:     White,
   5:     Black
   6: }

再看看编译器为上面的代码做了些什么事情:

   1: .class public auto ansi sealed Color
   2:     extends [mscorlib]System.Enum
   3: {
   4:     .field public static literal valuetype Color Black = int32(2)
   5:     .field public static literal valuetype Color Red = int32(0)
   6:     .field public specialname rtspecialname int32 value__
   7:     .field public static literal valuetype Color White = int32(1)
   8: }

 

原来是定义了一个从System.Enum派生的类型啊,从System.Enum的代码我们可以看到,System.Enum是一个抽象类,但这个抽象类是从System.ValueType派生而来的。 所以枚举也是一个值类型。

值类型一定是分配在栈上么?

记得园子里也有相关的讨论。一般书上都讲值类型是分配在栈上的,而引用类型是分配在堆上的。不过这要看值类型的使用方式,如果值类型作为方法的局部变量或者方法的参数,那么值类型才分配在栈上,而如果值类型作为引用类型的字段,那么该值类型则分配在堆上:

   1: public class MyObject
   2: {
   3:     //作为引用类型的字段使用,该值类型会分配在堆上
   4:     private int _objectField;
   5:     //valueParameter作为方法的参数分配在栈上
   6:     public void Test(int valueParameter)
   7:     {
   8:         //作为方法的局部变量,分配在栈上
   9:         int localVar = 5;
  10:     }
  11: }

 

那值类型里如果“有一个引用类型”,该引用类型也分配在栈上么?

实际上“有一个引用类型”这个说法本来就不正确,值类型里是指向这个引用类型实例的引用(指针),这个指针指向的是堆上的引用类型所在的内存区域。