老生常谈 -- 什么是装箱什么是拆箱
我们知道.NET 具有两个数据类型:值类型和引用类型。因为值类型没有指针引用,不是分配在托管堆中,也不会被 GC 回收,因此它比引用类型更加高效。但有时我们需要将一种类型的变量转换为另一种类型,这时我们就可以使用装箱/拆箱。
一、什么是装箱
装箱就是将值类型的数据存储在引用类型的变量中。例如在方法中创建了 int 类型的变量,需要将这个值类型赋值给一个引用类型的变量,这就意味着对这个值进行了装箱操作,代码如下:
上面的代码就是将值类型分配给 object 类型变量的过程,这个就是装箱操作。当我们对一个值进行装箱时,CoreCLR 会在堆上分配新对象,并将这个值类型的值复制到新分配的对象实例上,然后返回托管堆中新分配对象的引用。
二、什么是拆箱
将装箱反过来操作就是拆箱,也就是将引用类型变量的值转换回栈中值类型的过程。CoreCLR 首先会验证接收的数据类型是否等同于被装类型,如果是就把值复制回基于栈存储的变量中。例如下面的代码中,objNum 的底层类型是 int,就完成了拆箱操作:
Tip:与普通的类型转换不同,我们必须将其拆箱到一个恰当的数据类型中。如果我们将数据拆箱到不正确的数据类型中,会抛出 InvalidCastException 异常。因此为了安全起见,如果不能保证 Object 类型背后的类型,那么应该使用 try/catch 语句把拆箱操作包起来。
三、IL 代码
当编译器遇到装箱/拆箱语法时,它会生成包含装箱/拆箱操作的 IL 代码。使用 ildasm.exe 查看编译的程序集就会看到装箱和拆箱操作对应的 box 和 unbox 指令:
上面的 IL 代码中来看,装箱/拆箱似乎是一个没用的特性。因为我们很少需要在 Object 变量中存储值类型。但是实际是装箱/解箱过程是有用的,它允许假设一切都可以被当作 Object 类型来处理,CoreCLR 会帮我们处理与内存有关的细节。
四、总结
从程序员角度来看装箱和拆箱是非常方便的,不需要手动去复制和转移内存中的值类型和引用类型的数据。但是装箱/拆箱背后的栈/堆内存转移也会带来性能问题。以下总结了简单的整型数进行装箱和拆箱的步骤:
在托管堆中分配新对象;
在栈中的数据值被转移到该托管堆中的对象上;
当拆箱时,存储在堆中对象上的值被转移回栈中;
堆上未使用的对象将最终被 GC 回收。
很多时候装箱和拆箱操作不会在性能方面造成重大影响,但是如果一个类似于 ArrayList 这样的集合包含成千上万条数据,而程序又会频繁操作这些数据,性能的影响就会很明显的。因此在平时编程时应尽量避免发生装箱/拆箱操作。
评论