当一个值类型转换为对象类型时,则被称为 装箱;用于在垃圾回收堆中储存值类型。装箱是值类型到Object类型或到此类型所实现的任何接口类型的 隐式转换。
当一个对象类型转换为值类型时,则被称为 拆箱。从object类型到值类型或从接口类型到实现该接口的值类型的 显示转换。
利用装箱和拆箱功能,通过允许值类型的任何值与Object类型的值进行相互转换,将引用 类型与值类型连接起来。
⚠只有装过箱的对象才能拆箱;
为什么需要装箱(为何要将值类型转换为引用类型)
一种最普通的场景是调用一个包含类型为Object的参数的函数(方法),该Object可支持任意 类型,以便通用。当你需要将一个值类型传入容器时,就需要装箱了。
另一种的用法,就是一个泛型 的容器,同样是为了保证通用,而将元素定义为Object类型的,将值类型的值加入该容器时,需要装箱。
装箱/拆箱的内部操作
装箱: 对值类型在堆中分配一个对象实例,并将该值复制到新的对象中。
按三步进行:
第一步:在堆上分配一个内存空间,大小等于需要装箱的值类型对象的大小加上两个引用类型对象都拥有的成员:类型对象指针和同步块引用。
第二步:将值类型的实例字段拷贝到新分配的内存中。
第三步:返回一个指向堆上新对象的引用,并且存储到堆栈上被装箱的那个值类型的对象里。
这个地址就是一个指向对象的引用了。 有人这样理解:如果将Int32装箱,返回的地址,指向的就是一个Int32。我认为也不是不能这样理解, 但这确实又有问题,一来它不全面,二来指向Int32并没说出它的实质(在托管堆中)。
拆箱:所谓的拆箱,就是装箱操作的反操作,把堆中的对象复制到堆栈中,并且返回其值。需要注意的是,拆箱操作将判断被拆箱的对象类型和将要被复制的值类型引用是否一致,检查对象实例,确保它是给定值类型的一个装箱值。将该值从实例复制到值类型变量中。如果不一致,将会抛出一个InvalidCastException的异常。这里的类型匹配并不采用任何显示的类型转换。【以下代码说明了这一特性】
有书上讲,拆箱只是获取引用对象中指向值类型部分的指针,而内容拷贝则是赋值语句之触发。
static void Main(string[] args) { try { Int32 i = 3; // 装箱 Object o = i; // 拆箱,类型转换失败 Int16 j = (Int16)o; } catch (Exception ex) { Console.WriteLine(ex.Message); } Int32 ii = 3; // 装箱 Object obj = ii; // 拆箱 Int16 jj = (Int16)(Int32)obj; Console.WriteLine("拆箱成功!"); Console.ReadKey(); }
装箱与拆箱对执行效率的影响
装箱和拆箱操作常发生在以下两个场合:
- 值类型的格式化输出。
- System.Object类型的容器。
第一种情况,值类型的格式化输出往往会涉及一次装箱操作。例如下面的两行代码:
int i = 10; Console.WriteLine("i的值是:" + i);
代码完全能够通过编译并且正确执行,但却引发了一次不必要的装箱操作。在第2行代码上,值类型i被作为一个System.Object对象传入方法之中,这样的操作完全可以通过下面的改动来避免:
int i = 10; Console.WriteLine("i的值是:" + i.ToString());
改动后的代码调用了i的ToString()方法来得到一个字符串对象。由于字符串是引用类型,所以改动后的代码就不在涉及装箱操作。
第二种情况更为常见一些。例如常用的容器类ArrayList,就是一个典型的System.Object容器。任何值类型被放入ArrayList的对象中,都会引发一次装箱操作。而对应的,取出值类型对象就会引发一次拆箱操作。在.NET 1.1之前,这样的操作很难避免,但在.NET 2.0推出了泛型的概念后,这些问题得到了有效的解决。泛型允许定义针对某个特定类型(包括值类型)的容器,并且有效的避免装箱和拆箱。
因此,从原理上可以看出,装箱时,生成的是全新的引用对象,这会有时间损耗,也就是造成效率降低。因此如何避免装箱拆箱操作,是程序员在编写代码时需要时刻考虑的一个问题,那该如何做呢?
首先,应该尽量避免装箱。可以通过重载函数来避免,也可以通过泛型来避免。
当然,凡事并不能绝对,假设你想改造的代码为第 三方程序集,你无法更改,那你只能是装箱了。
对于装箱/拆箱代码的优化,由于C#中对装箱和拆箱都是隐式的,所以,根本的方法是对代码进行分析,而分析最直接的方式是了解原理结构。
值类型和引用类型的转换需要进行的操作:
将值类型转换为引用类型,需要进行装箱操作需要进行下面的过程:
1)、首先从托管堆中为新生成的引用对象分配内存。
2)、然后将值类型的数据拷贝到刚刚分配的内存中。
3)、返回托管堆中新分配对象的地址。
可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。
将引用内型转换为值类型,需要进行拆箱操作需要进行下面的过程:
1)、首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。
2)、将引用对象中的值拷贝到位于线程堆栈上的值类型实例中。
经过这2步,可以认为是同boxing是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝 数据的操作就会同boxing操作中一样影响性能。
相对于简单的赋值而言,装箱和取消装箱过程需要进行大量的计算。 对值类型进行装箱时,必须分配并构造一个新对象。 取消装箱所需的强制转换也需要进行大量的计算,只是程度较轻。