谈谈C#中的装箱和拆箱

今天,来谈谈C#中的装箱和拆箱的知识,算是一个总结。

之前说过,在C#中,有两个类型:值类型和引用类型。值类型属于一种轻类型,它不像引用类型的变量会在托管堆上分配内存,不会被垃圾回收,它没有堆上的每个对象都有的额外成员(类型对象指针和同步块索引)。但是在现实生活中,我们还是要获取对值类型的一个实例的引用。

还是用那个经典的例子ArrayList。

这个ArrayList是Array的强化版,它不同于数组。数组要求每个索引的数据是同一类型的,而ArrayList则不然,它允许插入的数据类型不一致。

   1:  public virtual int Add(object value)
   2:  {
   3:      if (this._size == this._items.Length)
   4:      {
   5:          this.EnsureCapacity(this._size + 1);
   6:      }
   7:      this._items[this._size] = value;
   8:      this._version++;
   9:      return this._size++;
  10:  }

这个ArrayList中有一个方法Add,就是向ArrayList实例的尾部添加一个数据。这里最关键的是,这个添加的数据的类型是老大哥object,这也是为什么ArrayList可以添加各种数据类型的一个重要原因。

那么,当我们向ArrayList中添加一个int类型的数据时,是不是有

public virtual int Add(int value)
{
    ......
}

这样的重载方法呢?很不幸,并没有。

这也就是说明,当我们添加一个int类型数据时,实际上最终的IL代码是添加了object类型的数据。其实,这就是这次所讲的装箱操作。

装箱是指将值类型转换为引用类型,拆箱是指将引用类型转换为值类型。

值类型

在装箱操作中,内部发生了以下几件事:

Step1:在托管堆上分配内存

Step2:值类型的字段复制到新分配的堆内存

Step3:返回对象的地址

C#编译器会自动生成将一个值类型的实例装箱所需要的IL代码。

拆箱

乍一看,拆箱即是装箱的逆操作。其实,这就大错特错了,因为仔细观察拆箱的逆操作,就会发现里面有些步骤是不需要的。

在拆箱操作中,内部只发生了以下两件事:

Step1:获取被装箱完毕(boxed)值(现在叫做对象咯)的地址

Step2:将这个地址引用的值复制到基于栈的值类型实例中

将值类型的Step倒过来看,就会发现最本质的区别是拆箱操作不需要将值复制到内存中,因为值类型压根就不需要分配内存,它是直接存储在栈上。

综述

从上面的分析可以看出,无论是装箱和拆箱都会对应用程序的速度和内存消耗产生不利影响,这里面涉及到堆和栈的转换。所以在现实生活中,装箱和拆箱操作使用得谨慎。

像上文中的ArrayList.Add就是装箱的一个例子,在数据量较大的时候对性能的影响很明显的。

在一般的编码中,我们会用List<T>来替代。

ArrayList版本

   1:  ArrayList arrayList=new ArrayList();
   2:  for(int i=0;i<50000;i++)
   3:  {
   4:      arrayList.Add(i);
   5:  }

List<T>版本

   1:  List<int> list = new List<int>();
   2:  for (int i = 0; i < 50000; i++)
   3:  {
   4:      list.Add(i);
   5:  }

后者避免了50000次的装箱操作,如果次数更大的时候,性能影响就更明显了。所以,就我而言,在现实中我基本没用过ArrayList,List<T>泛型类可以解决绝大部分事情,而且还有一些额外的优势,这个以后会说。

补充

CLR还允许将一个值类型拆箱为同一个值类型的可空版本。

   1:  Int32? x=5;
   2:  Int32 y=(Int32)x;

可空值类型int32?等价于Nullable<Int32>。

posted @ 2011-10-27 20:26  Rivers Bian  阅读(332)  评论(0编辑  收藏  举报