您的位置:首页 > 其它

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

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

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

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

public class MyValueType : ValueType


{


//some code


}


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

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

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

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

public struct MyValueType


{




}


就这样,你的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枚举的三个成员:

[Serializable, ComVisible(true)]


public enum LayoutKind


{


 //CLR自动控制内存布局


 Auto = 3,


 //使用偏移量在代码中显式控制布局


 Explicit = 2,


 //按照开发人员书写代码时的字段顺序控制


 Sequential = 0


}


如果使用class定义一个类型,C#编译器默认会使用LayoutKind.Auto,而如果定义一个struct,C#编译器默认会使用LayoutKind.Sequential。因为.NET中的struct的存在实际上是为了与哪些非托管代码交互的,这个时候就必须知道你的内存布局是个啥样子的,如果你使用Auto这种方式,那布局就是由CLR自动控制,你不知道CLR到底是如何自动的,也就无法交互了。不过,如果你确定你定义的这个struct不会与非托管代码交互,你也可以使用如下这样的代码覆盖C#编译器的默认设置了:

[StructLayout(LayoutKind.Sequential)]


public class MyObject


{ 




}


[StructLayout(LayoutKind.Auto)]


public struct MyValueType


{


}


下面是对应的IL代码:

.class public sequential ansi beforefieldinit MyObject


 extends [mscorlib]System.Object


{


}




.class public auto ansi sealed beforefieldinit MyValueType


 extends [mscorlib]System.ValueType


{


}


现在换了个儿,class的使用sequential,而struct使用auto(注意,LayoutKind和StructLayout都来自System.Runtime.InteropServices命名空间,请添加对应的using指令)。

CLR还允许你使用LayoutKind.Explicit配合FieldOffsetAttribute显式的指定每个字段的偏移(不过一般请不要这样做)。代码如下:

[StructLayout(LayoutKind.Explicit)]


public struct MyValueType


{


 [FieldOffset(0)]


 public int A;




 [FieldOffset(4)]


 public int B;


}


关于这个的更多示例可以参见MSDN

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

public enum Color


{ 


 Red,


 White,


 Black


}


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

.class public auto ansi sealed Color


 extends [mscorlib]System.Enum


{


 .field public static literal valuetype Color Black = int32(2)


 .field public static literal valuetype Color Red = int32(0)


 .field public specialname rtspecialname int32 value__


 .field public static literal valuetype Color White = int32(1)


}


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

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

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

public class MyObject


{


 //作为引用类型的字段使用,该值类型会分配在堆上


 private int _objectField;


 //valueParameter作为方法的参数分配在栈上


 public void Test(int valueParameter)


{


 //作为方法的局部变量,分配在栈上


 int localVar = 5;


}


}


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

实际上“有一个引用类型”这个说法本来就不正确,值类型里是指向这个引用类型实例的引用(指针),这个指针指向的是堆上的引用类型所在的内存区域。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐