装箱与拆箱(又叫取消装箱)就是值类型与引用类型的转换,是值类型和引用类型之间的桥梁。
之所以可以这样转换是因为 CTS允许这样做。只有值类型才存在装箱和拆箱。
装箱是隐式的,拆箱是显式的,因为你需要告诉 CLR你要给拆出来的值赋予什么类型。
通过深入了解装箱与拆箱的过程,我们可以知道其中包含了对堆上内存的操作,故会消耗性能,这是完全不必要的。
另外值得注意的是,装箱需要比原数据更多的空间,因为它需要两个引用类型的标准配置:类型对象指针和同步块索引。
装箱
装箱转换是指将一个值类型隐式地转换成一个object
类型,或者把这个值类型转换成一个被该值类型应用的接口类型interface-type。把一个值类型的值装箱,也就是创建一个object
实例并将这个值复制给这个object
。比如:
int i = 10;
object o = i;
具体过程:
- 在堆中申请内存,内存大小为值类型的大小,再加上额外固定空间(类型对象指针和同步块索引)。
- 将值类型的字段值拷贝到新分配的内存中。
- 返回新引用对象的内存地址(给栈上的引用)。
我们可以从图中看到,装箱就是生成图中除了一开始i=10
的变量之外另外两块变量的过程。
实际上,仅仅通过观察C#代码,是无法意识到装箱的,只有访问对应的IL代码才能真正观察到装箱。
IL代码的装箱指令为box。上面两行代码对应的IL代码为:
IL_0040: ldc.i4.1
IL_0041: stloc.2
IL_0042: Idloc.2
IL_0043 : box [mscorlib] System.Int32
IL_0048: stloc.3
其中前两行对应int i=10
这句代码,后三行对应object o=i
这句代码。
另外装箱的过程,我们也可以用显式的方法来进行装箱操作:
int i = 10;
object obj =(object)i;
拆箱
拆箱转换是指将一个对象类型显式地转换成一个值类型,或是将一个接口类型显式地转换成一个执行该接口的值类型。
拆箱的过程分为两步:首先,检查这个对象实例,看它是否为给定的值类型的装箱值。然后,把这个实例的值拷贝给值类型的变量。
int b = (int) o;
具体过程,如下:
- 在CLR via C#中,拆箱被定义为第一步。下面的代码就是拆箱:
(int) o;
。最后获得对象各个成员的地址
同时,由于并非一定成功,所以存在抛出异常的可能:- 检查是否为Null,否则抛出
NullReferenceException
异常。 - 检查实例是否为给定值类型的装箱值。否则抛出
InvalidCastException
异常。
- 检查是否为Null,否则抛出
- 创建一个新的对象b,并将第一步获得的值复制到b中。
可以看出拆箱过程正好是装箱过程的逆过程。必须注意,装箱转换和拆箱转换必须遵循类型兼容原则。
另外,IL代码的拆箱指令为unbox。拆箱对应的IL代码为:
IL_000a: Idloc.1
IL_000b: unbox.any [mscorlib]System.Int32
IL_0010: stloc.2
与拆箱比较,装箱的性能消耗更大,因为引用对象的分配更加复杂,成本也更高,值类型分配在栈上,分配和释放的效率都很高。装箱过程需要创建一个新的引用类型对象实例。
尽量避免拆箱和装箱
我们之所以研究装箱和拆箱,是因为装箱和拆箱会造成相当大的性能损耗(相比之下,装箱要比拆箱性能损耗大),性能问题主要体现在执行速度和字段复制上。因此我们在编写代码时要尽量避免装箱和拆箱,常用的手段为:重载、泛型、ToString
、提前显式装箱。
重载
使用函数重载的方法。为了避免装箱,很多FCL中的方法都提供了很多重载的方法。比如Console.WriteLine
方法,提供了多达19个重载方法,目的就是为了减少值类型装箱的次数。比如看下面的这段代码:
Console.WriteLine(3);
刚开始你可能觉的3
会装箱为string
类型,但是实际上这条语句不会进行装箱操作,是因为Console.WriteLine
方法有一个重载的方法,参数就是一个int
的值。
public static void WriteLine(int value);
类似Console.WriteLine
方法,还有System.IO.BinaryWriter
的Write
方法,System.IO.TextWriter
的Write
和WriteLine
方法,System.Text.StringBuilder
的Append
和Insert
方法等都提供了大量的重载方 法,以减少装箱次数。所以我们在实际的项目中,应该时刻注意装箱的情况,并且选用合适的重载方法避免装箱。
泛型
使用泛型。因为装箱和拆箱的性能问题,所以在.NET 2.0中引用了泛型,他的主要目的就是避免值类型和引用类型之间的装箱和拆箱。我们常用的集合类都有泛型的版本,比如ArrayList
对应着泛型的List<T>
,Hashtable对应着Dictionary<TKey, Tvalue>
。
方法ToString
使用函数方法ToString
。这点单独列出来是因为虽然小,但是很实用。虽然表面上看值类型调用ToString
方法是要进行装箱的,因为ToString
是从基类继承的方法。但是ToString
方法是一个虚方法,值类型一般都重写了这个方法,所以调用ToString
方法不会装箱。之前说过String.Format
方法容易造成装箱,避免的最佳方法就是在调用这个方法前将所有的值类型参数都调用一次ToString
方法。
提前显式装箱
如果在项目中一个值类型变量需要多次拆装箱,那么可以将这个变量提出来在前面显式装箱。比如下面这段代码:
int j = 3;
ArrayList a = new ArrayList();
for (int i = 0; i < 100; i++)
{
a.Add(j);
}
可以修改为:
int j = 3;
object ob = j;
ArrayList a = new ArrayList();
for (int i = 0; i < 100; i++)
{
a.Add(ob);
}
参考链接: