C#系列学习笔记4:装箱和拆箱

c#中的数据类型可以分为 值 和 引用。

值 包括byte,short,int,long,float,double,decimal,char,bool,enum,struct,变量直接存储数据,编译器分配内存 ;

引用。string,object,和 class 统称为引用类型。当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。

实际上 值 和 引用 都由基类System.Object继承而来,因此可以通过装箱和拆箱来相互转换。

装箱:值 -> 对象。

int val = 100; 
object obj = val; 

对值类型在堆中分配一个对象实例,并将该值复制到新的对象中。按三步进行:

  1. 首先从托管堆中为新生成的引用对象分配内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)。
  2. 然后将值类型的数据拷贝到刚刚分配的内存中。
  3. 返回托管堆中新分配对象的地址。这个地址就是一个指向对象的引用了。

可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。

boxing
PS:o 和 i 的改变将互不影响,因为装箱使用的是 i 的一个副本

拆箱:对象 -> 值。

int val = 100; 
object obj = val; 
int num = (int) obj;

1、首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。
2、将引用对象中的值拷贝到位于线程堆栈上的值类型实例中。
经过这2步,可以认为是同boxing是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同boxing操作中一样影响性能。

Unboxing
PS:o 和 i 的改变将互不影响

什么时候会发生拆箱和装箱?
CLR对值类型进行装箱时,会将值包装在 System.Object 实例中并将其存储在托管堆中。拆箱将从对象中提取值类型。 装箱是隐式的;拆箱是显式的。

此外,例如我们要将一组坐标点存入ArrayList中,坐标点用struct(值)来表示:

struct Point{
    double x;
    double y;
}

Point point;
point.x = 10.0;
point.y = 10.0;

...
ArrayList arr;
arr.Add( point );   // Add()方法接受的参数是一个object(引用类型),Point是一个值类型
                    // 此时会进行一次装箱操作,传入的值类型会转变为引用类型
                    // 旧的point对象不变,装箱的本质是重新建立一个point对象(引用类型)
...

从上述集合中获取点的信息,获取ArrayList[0]包含的引用(或指针),并将它放到Point对象的实例pFirst中,发生拆箱

Point pFirst = arr[0];  // 发生拆箱,pFirst与arr[0]互不干扰

当然,目前在C#中,肯定不会再继续用非泛型的ArrayList来存储一些对象的集合了,因为有了新的泛型集合List<T>,在使用的时候规定是什么类型,不需要在存取数据的时候进行多余的装箱和拆箱操作。不过在写代码的时候还是会隐藏很多拆箱和装箱的过程,注意尽量避免装箱和拆箱的操作。如果不可避免,那就尽量减少装箱和拆箱的操作