还是用那个经典的例子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>。
评论