装箱和拆箱
值类型变量在线程堆栈上分配存储空间,然而由于其派生自Object类,所以可以用一个Object类变量存放一个值类型数据。请看以下代码:
int i = 123;
object o = i;
很明显,第2句代码将值类型的数据“123”放到了一个Object类型的变量o中,而o是一个引用类型变量,其引用的对象必须存活于托管堆中。为了解决这个问题,CLR将值类型的数据“包裹”到一个匿名的托管对象中,并将此托管对象的引用放在Object类型的变量o中,这个过程称为“装箱(Boxing)”,装箱后对象的内存布局如图4-4所示。
事实上,CLR为所有的值类型变量都提供了一个对应的“装箱”数据类型,此数据类型其实是一个类,拥有与值类型相同的数据和行为。
需要注意的是,装箱之后的变量i与变量o是两个完全独立的变量,只是初值相同罢了,对任何一个变量值的改变不会影响另一个。
1 int i = 123;
2 object o = i; // 装箱:boxing
3 int j = (int) o; // 拆箱:unboxing
上述第3句代码将装箱后的数据再“拆箱(Unboxing)”,将其值赋给变量j,参见图4-5。
“装箱”与“拆箱”使我们可以把值类型变量看成是引用类型变量,但这个操作是耗时的,会影响程序的运行性能,因此,应该尽量避免在程序中使用装箱与拆箱操作。
深入内幕
装箱与拆箱的技术内幕
(1)装箱
装箱转换允许将值类型(Value Type)变量隐式转换为引用类型(Reference Type)变量。
将值类型变量的一个值装箱包括以下操作:分配一个对象实例,然后将值类型变量的值复制到该实例中。
可以用以下方法理解实际装箱过程:设想有一个特殊的装箱类(Boxing Class)。对任何值类型的类型T而言,装箱类的行为可描述如下:
sealed class T_Box: System.ValueType
{
T value;
public T_Box(T t)
{
value = t;
}
}
下面的装箱语句:
int i = 123;
object box = i;
在概念上相当于
int i = 123;
object box = new int_Box(i);
实际上,像上面这样的T_Box和int_Box并不存在,并且装了箱的值的动态类型也不会真的属于一个类类型。相反,类型T的装了箱的值属于值类型T。例如:
int i = 123;
object box = i;
if (box is int) //装箱后的变量仍是int类型
{
Console.Write("Box对象包含一个int型数据");
}
上述代码在运行时将在控制台上输出字符串“Box对象包含一个int型数据”。
装箱转换隐含着复制一份待装箱的值。这不同于从引用类型到Object类型的转换,在后一种转换中,转换后的值继续引用同一实例,只是将它当作派生程度较小的Object类型而已。例如,以下代码声明了一个值类型Point。
struct Point
{
public int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
则下面的语句
Point p = new Point(10, 10);
object box = p;
p.x = 20;
Console.Write(((Point)box).x);
将在控制台上输出值10,因为将p赋值给box是一个隐式装箱操作,它将复制p的值。但如果将Point声明为class,由于p和box将引用同一个实例,因此输出值为20。
(2)拆箱
拆箱是装箱的逆过程。
一个拆箱操作包括以下两个步骤:
检查对象实例是否为给定值类型的一个装了箱的值。
将该值从实例中复制出来。
参照前面的例子,下面的语句
object box = 123;
int i = (int)box; //拆箱
在概念上相当于
object box = new int_Box(123);
int i = ((int_Box)box).value;
为了使给定值类型拆箱转换在运行时取得成功,源操作数的值必须是对某个对象的引用,而该对象先前是通过将该值类型的某个值装箱而创建的。
q 如果源操作数为null,则将引发System.NullReferenceException异常。
q 如果源操作数是对不兼容对象的引用,则将引发System.InvalidCastException异常。
IL为装箱与拆箱分别准备了两条指令:Box和Unbox,读者可以查询Visual Studio 2005文档了解详情。