值类型的装箱和拆箱
值类型是比引用类型更“轻型”的一种类型,因为它不需要作为对象在托管堆中分配,不会被垃圾回收,也不通过指针来引用。
static void Main()
{
System.Collections.ArrayList list = new System.Collections.ArrayList();
Point p;//分配一个Point,不在堆中分配。
for (int i = 0; i < 20; i++)
{
p.x = p.y = i;//初始化值类型的成员
list.Add(p);//对值类型进行装箱,并将引用添加到ArrayList中。C#编译器会自动的生成对一个值类型的实例进行装箱所需的IL代码,C#编译器检测到是向一个需要引用类型的方法传递一个值类型,所以会自动生成代码对对象进行装箱。在运行时,当前存在于Point值类型实例p中的字段会复制到新分配的Point对象中。已装箱的Point对象(已经是引用类型)的地址会返回给Add方法。Point对象会一直存在于堆中,直到被垃圾回收。Point值类型的变量p可以重用,因为ArrayList根本不知道关于它的任何事情。
Point p1 = (Point)list[0];//拆箱 ,
}
}
List.Add()方法需要一个object参数,Add需要获取对托管堆上的一个对象的引用(或指针)来作为参数。在代码中传递的是p,是一个值类型,为了使代码正确工作,Point已经进行了一次装箱。
对一个值类型的实例进行装箱操作时发生在内部的事情:
1.在托管堆中分配内存。
2.值类型的字段复制到新分配的堆内存中。
3.返回对象的地址,现在这个地址是对一个对象的引用,值类型现在是一个引用类型。
拆箱发生的事:
1.获取已装箱的对象的各个字段的地址。
2.将字段包含的值从堆中复制到基于栈的值类型实例中。
拆箱的代价比装箱的低得多。拆箱就是获取一个指针的过程,该指针包含在一个对象中的原始值类型,事实上,指针指向的是已装箱实例中未装箱的部分,所以和装箱不同,拆箱不要求在内存中复制任何字节,往往会紧接着拆箱操作发生一次字段的复制操作。
装箱和拆箱会对应用程序的速度和内存消耗产生不利影响,所以应该注意编译器在什么时候生成代码来自动这些操作,并尝试手动编写代码,尽量避免自动生成代码的情况。
static void Main()
{
Int32 i = 5;
object obj = i;//装箱
Int16 n = (Int16)obj;//拆箱失败,抛出InvalidCastException异常。如果引用指向的对象不是所期待的值类型的一个已装箱实例,就抛出此异常。
Int16 nn = (Int16)(Int32)obj;//先拆箱为正确的类型,在进行转型。
Int32 d = (Int32)obj;//
Console.ReadKey();
}
在对一个对象进行拆箱时,只能将其转型为原先未装箱时的值类型。
static void Main()
{
Int32 i = 5;
object obj = i;//装箱
i = 123;
Console.WriteLine(i + "," + (Int32)obj);//发生了多少次装箱?WriteLine()需要获取一个String对象,但是当前没有String对象,所以采取
//某种方式对这些数据进行合并,创建一个String对象。
//为了创建String对象编译器生成代码来调用String对象的静态方法Concat。
//String.Concat方法有几个重载版本,所有版本的执行操作都是一样的,不同的是参数数量。
//这里连接3个数据项创建一个字符串,所以编译器选择的是
//public static String Concat(object obj0,object obj1,object obj2) 这个版本
//为第一个参数传递的是i,i是一个未装箱的值类型,装箱。
//obj1是一个String对象的引用。obj3要求一次拆箱操作。然后装箱。
Console.WriteLine(i.ToString() + "," + obj);//这里没有装箱操作。i调用ToString方法,返回一个String,String对象已经是引用类型,所以能直接传递给Concat方法,不需要任何装箱操作
Console.ReadKey();
}
看看FCL,会发现许多方法都针对不同的值类型参数进行重载。大多数方法进行重载的唯一目的就是减少常值类型的装箱操作次数。
未装箱的值类型比引用类型更“轻型”,归结于以下两个原因:
1.值类型不再托管堆上。
2.值类型没有堆上的每个对象都有的额外成员:一个类型对象指针和同步块索引。
因为值类型没有同步块索引,所以哈,不能使用System.Threading.monitor类型的各种方法,实现不了同步。
在C#中只用使用接口才能修改一个已装箱的值类型中的字段,详情见126页。