接上一篇.net框架读书笔记---值类型与引用类型(一),这节主要学习值类型的装箱与拆箱;
先大体说一下:
- 装箱:值类型到引用类型的转换
- 拆箱:引用类型到值类型的转换
先看一下代码:


{
ArrayList a = new ArrayList();
Point p; // 分配一个Point(不在托管堆上)
for ( int i = 0 ; i < 10 ; i ++ )
{
p.x = p.y = 1 ; // 初始化值类型的成员
a.Add(p); // 对值类型进行装箱操作并将得到的引用添加到arraylist上
}
}
struct Point
{
public int x, y;
}
上面代码每一次循环都初始化一个Point值类型实例,并将其存储到ArralyList中,而查看ArrayList类型的Add方法原型如下:
可见Add接受一个object类型的参数,表示Add需要一个指向托管堆中对象的引用(或指针),而Point类型确是一个值类型,上面的代码之所以能够正确运行,首先必须将Point值类型实例转换为一个真正的托管堆上的对象,然后再将该对象的引用传递给Add方法;
在.net框架中,一种称作为装箱(boxing)的机制用来将一个值类型转换为一个引用类型,装箱操作通常由以下几个步骤组成:
- 从托管堆中为新生成的引用类型分配内存。分配内存的大小为,值类型实例本身的大小加上其他额外的将该值类型实例视为真正的引用类型对象所需的空间;
- 将值类型实例的字段拷贝到托管堆上新分配对象的内存中;
- 返回托管堆中新分配对象的地址,该地址就是一个指向对象的引用,值类型实例也就变成了一个引用类型对象;
其实装箱操作CLR会自己来做的,知道其原理会让我们写出高效的代码,避免性能的损失,现在开始拆箱(unboxing),假设我们在另一端代码中希望获取前面ArrayList中存放的第一个元素,如下:
这里首先得到的是包含在ArrayList中第一个元素的引用(指针),然后我们又试图将它赋值给一个Point值类型(p),要是上面代码正确运行,所有包含在已经装箱的Point对象中的字段都必须被拷贝到值类型变量p中,其中值类型变量p位于当前线程的堆栈上。CLR通过两个步骤来完成这样的拷贝操作,它首先获取已经装箱Point对象中、属于Point值类型的那部分字段的地址。这个过程称为拆箱(unboxing)。然后将这些字段的值从托管堆拷贝到位于线程堆栈上的值类型实例中。
拆箱和装箱并不是严格意义上的互反操作.拆箱操作的代价要比装箱操作小得多。拆箱操作仅仅是获取指向对象中包含的值类型部分(数据字段)的指针而已,它不会像装箱操作那样涉及到任何内存字节的拷贝。然而紧接着拆箱之后的典型的操作往往就是字段的拷贝,这两个操作和起来与装箱操作才是真正的互反;
对一个应用类型的拆箱操作通常由一下几步组成:
- 如果该引用为null,将会抛出一个NullReferenceException异常;
- 如果该引用指向的对象不是一个期望的类型的已装箱对象,将会抛出一个InvalidCastException异常;
- 一个指向包含在已装箱对象中值类型部分的指针被返回。
请看一下代码:
{
Int32 x = 5 ;
Object o = x;
Int16 y = (Int16)o; // InvalidCastException异常
}
当对一个对象执行拆箱操作时,转型的结果必须是它原来未装箱时的类型,正确做法
{
Int32 x = 5 ;
Object o = x;
Int16 y = (Int16)(Int32)o; // 先正确拆箱,然后再转型
}
严格的讲,拆箱操作不会拷贝任何字段。但通常情况下,拆箱操作后会紧跟着一个字段的拷贝操作,将字段从托管堆中拷贝到线程堆栈中。


{
Point p;
p.x = p.y = 1 ;
object o = p; // 对p进行装箱;o指向已装箱对象
p = (Point)o; // 对o进行拆箱,并将字段从托管堆中拷贝到堆栈上
}
对于上面最后一行代码,C#编译器会产生一个对O执行拆箱的IL指令(获取对象中属于值类型部分的字段地址),以及另一个将字段从托管堆拷贝到堆栈变量p上的IL指令。