第5章 基元类型、引用类型与值类型 (2)

5.2 引用类型与值类型
引用类型(reference type)总是从托管堆上分配,而值类型通常分配在线程堆栈上,不受垃圾收集器的控制,减少了托管堆的压力以及应用程序在整个生存期中需要垃圾回收的次数

所有的值类型都继承自System.ValueType,而System.ValueType继承自System.Object。它重写了System.Object中的Equals方法和GetHashCode方法。当定义自己的值类型时我们也应重写Equals方法和GetHashCode方法,为它们提供一个显式的实现

定义值类型时不能为其选择任何的基类型,但可以作为一个或多个接口的实现。此外CLR不允许一个值类型被作为其他任何引用类型或值类型的基类

值类型在new的时候C#编译器会在线程的堆栈上分配一个实例,C#还会确保该值类型实例的所有字段都被设置为二进制意义上的0。如果只值类型声明而不初始化,编译器将产生相同的IL指令,唯一的差别是使用了new操作符时C#会认为实例已经得到了初始化

如果类型满足以下这些条件,我们就应该考虑将其声明为值类型:
   1、该类型的行为类似于基元类型
   2、该类型不需要继承自任何其他类型也不会被任何其他类型继承
   3、该类型的实例不会频繁地用于方法的参数传递。因为默认情况下参数以传值的方式传递,会导致值类型实例中的字段被拷贝,损伤应用程序性能。同样该类型的实例也不应被作为方法的结果频繁的返回(引用类型的实例作为参数传值时为什么性能损失较小?)
   4、该类型的实例不会被频繁的用于诸如ArrayList、Hashtable之类的集合中。因为这些管理一组通用对象集合的类会对值类型实例执行装箱操作

我们不应向值类型中引入任何新的虚方法(实际上这样做在C#中是非法的),更不可以有任何的抽象方法,所有方法都隐含为sealed

未装箱的值类型因为没有分配在托管堆上,所以一旦定义其实例的方法不再处于活动状态,为实例分配的空间就会立即释放

CLR如何控制类型中字段的布局
为提高性能,CLR会对类型实例中的字段按一定方式排序,例如CLR可能会在内存中重新排列对象的字段,以使对象引用可以聚合在一起,并能恰当的对齐和包装数据字段。

System.Runtime.InteropServices.StructLayoutAttribute特性可以告诉CLR我们期望的排序方式,传入LayoutKind.Auto表示让CLR自己排列字段,传入LayoutKind.Sequential表示让CLR保留我们设定的字段布局。如果定义类型时没有显示指定该特性,编译器将选择默认的方式。C#编译器为引用类型选择的是LayoutKind.Auto方式而为值类型选择的是LayoutKind.Sequential方式

5.3 值类型的装箱与拆箱
装箱的过程:
   1、从托管堆中为新生成的引用类型对象分配内存,大小为值类型实例本身的大小加上额外的一个方法表指针和一个SyncBlockIndex的空间
   2、将值类型实例的字段拷贝到托管堆上新分配对象的内存中
   3、返回托管堆中新分配对象的地址

拆箱和装箱并不是严格意义上的互反操作。C#中拆箱之后总是紧跟一个字段拷贝操作,这两个操作合起来才与装箱是真正的互反操作

拆箱的过程:
   1、如果该引用为null,将会抛出一个NullReferenceException异常
   2、如果该引用指向的对象不是一个期望的值类型的已装箱对象,将会抛出一个InvalidCastException
   3、返回一个指向包含在已装箱对象中值类型部分的指针

当对一个对象执行拆箱操作时,结果必须是它原来未装箱时的类型,例如不能将一个已装箱的Int32对象直接转换为一个Int16值类型

   需要对值类型装箱的几种情况:
      1、因为未装箱的值类型没有SyncBlockIndex,所以不可能利用System.Threading.Monitor类型来同步多个线程对它们的访问
      2、因为未装箱的值类型没有方法表指针,所以我们不可能通过值类型的未装箱实例来调用其上继承而来的虚方法
      3、将一个未装箱的值类型实例转型为该类型实现的接口类型时需要对该实例进行装箱

posted on 2007-02-09 23:09  jiangnii  阅读(250)  评论(0编辑  收藏  举报