十五:值类型的装箱和拆箱(一)

     简单的说装箱是将值类型转换为引用类型拆箱是将引用类型转换为值类型,但其内部是怎么实现的,CLR又是如何工作的呢,看下面代码:

using System;

using System.Collections;

struct Point

{

    public Int32 x, y;

}   

public sealed class Program

{

    public static void Main()

    {

        ArrayList a = new ArrayList();

        Point p; //在线程堆栈中分配一个Point

        for (Int32 i = 0; i < 10; i++)

        {

            p.x = p.y = i; //初始化值类型中的成员

            a.Add(p);

        }

    }

}

     ArrayList中实际存储的是什么?是Point的结构,还是Point结构地址?先看看ArrayLIst的Add方法原型:

     public virtual Int32 Add(Object value);

显然,Add需要的是一个Object引用类型的参数,但在以上代码中传递的是一个值类型p。为了能正常工作,Point值类型必须转换成一个引用类型对象,而且要获得这个对象的一个引用,就对p进行装箱,以下是进行装箱时内部发生的事:

(1)从托管堆中分配好内存。分配的内存量是值类型的各个字段所需的内存量加上托管堆上的所有对象都有的两个额外成员(即类型对象指针和同步块索引)所需要的内存。

(2)值类型的字段复制到新分配的堆内存。

(3)返回对象的地址。

C#编译器会自动生成对一个值类型的实例进行装箱所需的IL代码。C#编译器检测到是向一个需要引用类型的方法传递一个值类型,所以会自动生成代码来对对象进行装箱。在运行时,当前存在于Point值类型实例p中的字段会复制到新分配的Point对象中,已装箱的Point对象的地址在返回之后,会传给Add方法,Point对象会一直存在于堆中,直到被垃圾器收集。Point值类型变量p可以重用,因为ArraryList根本不知道关于它的任何事情。

再看以下代码:

Point p = (Point)a[0];

获取ArraryList的元素0中包含的引用,并将其放到值类型的一个变量中。包含在已装箱的Point对象的所有字段都必须复制到线程堆栈上的值类型变量p中。CLR分两步完成这个复制操作,首先,获取已装箱的Point对象中的各个Point字段的地址,这个过程称为“拆箱”,然后,这些字段包含的值从堆中复制到基于堆栈的值类型中。

相对于装箱操作,拆箱操作代价要低得多。拆箱其实就是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型,也就是指向已装箱实例中的未装箱部分,所以,拆箱不涉及任何字节在内存中的副本。一个已装箱的值类型实例在拆箱时内部发生的事如下:

(1)包含已装箱的值类型的引用的变量如果为null,就抛出一个NullReferenceException异常。

(2)如果引用指向的对象不是所要求的值类型的一个已装箱的实例,就抛出一个InvaildCastException异常,如下代码:

Int32 x = 5;

Object o = x; //对x进行装箱,o引用已装箱的对象

Int16 y = (Int16)o; //抛出一个InvalidCastException异常

从逻辑上说,可以获取o所引用的一个已装箱的Int32,然后将其转型为一个Int16。然而,在对一个对象进行拆箱操作的时候,只能将其转型为未装箱时的值类型,在本例是Int32,如下是正确的:

Int32 x = 5;

Object o = x;   //对x进行装箱,o引用已装箱的对象

Int16 y = (Int16)(Int32)o; //先拆箱为正确的类型,再进行类型转换

 

 

 

posted @ 2009-01-28 12:06  Done  阅读(462)  评论(0编辑  收藏  举报