在上一篇中提到:装箱需要在托管堆上分配额外的对象,将来必须对其进行垃圾回收,所以不必要的,额外的装箱应尽量避免,否则会影响程序的性能和内存消耗,那么装箱是怎样影响程序性能的呢?
先来看一段示例代码:
struct Point { public Int32 x, y; } class Program { static void Main() { ArrayList a = new ArrayList(); Point p; for (Int32 i = 0; i < 10; i++) { p.x = 1; p.y = 2; a.Add(p); } Console.Read(); } }
ArrayList中的Add方法的原型:public virtual int Add(object value); 可以看出Add方法需要的参数是托管堆上的一个对象引用,所以Add方法要正确执行,必须将值类型对象p转换成托管堆中的对象,并得到这个对象的引用。这个转换过程就是我们说的装箱,下面总结了值类型实例进行装箱是发生的事情:
- 在托管堆上分配好内存,分配的内存量是值类型各字段需要内存量加上托管堆上所有对象都有的两个额外成员(类型对象指针和同步块索引)。
- 值类型的字段赋值到新分配的内存。
- 返回对象的地址。
实际上,我们的代码中已经很少用到ArrayList了,我们更多的是用List类,它是ArrayList的扩展和增强,而其中最大的增强就是泛型集合类允许开发人员在操作值类型的时候不在需要对集合中的成员装箱/拆箱处理了。它减少了托管堆中对象的创建,进而减少了程序需要垃圾回收的次数。
执行代码 Point p = (Point) a[0] 将一个引用类型的对象转换为值类型称之为拆箱,但是拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低得多。拆箱其实就是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。事实上,指针指向的是已装箱实例中的未装箱部分。所以,和装箱不同,拆箱不需要在内存中复制任何字节,而紧接着会发生一次字段的复制,将实例字段复制到栈变量中。
C#编译器隐式的生成代码对对象进行装箱,因此如果特别关注应用程序的性能,需要借助ILDasm工具查看IL代码,可以看到装箱的box指令和拆箱的unbox指令。
下面通过一段代码来测试下对装箱和拆箱的理解程度:
interface IChangeBoxedPoint { void Change(Int32 x, Int32 y); } struct Point : IChangeBoxedPoint { public Int32 m_x, m_y; public Point(Int32 x, Int32 y) { m_x = x; m_y = y; } public void Change(int x, int y) { m_x = x; m_y = y; } public override string ToString() { return string.Format("({0},{1})", m_x, m_y); } } class Program { static void Main() { Point p = new Point(1, 1); Console.WriteLine(p); p.Change(2, 2); Console.WriteLine(p); object o = p; Console.WriteLine(o); ((Point)o).Change(3, 3); Console.WriteLine(o); ((IChangeBoxedPoint)p).Change(4, 4); Console.WriteLine(p); ((IChangeBoxedPoint)o).Change(5, 5); Console.WriteLine(o); Console.Read(); } }
C#不允许更改已经装箱值类型的字段,但可以通过接口欺骗C#,将值类型转换为一个IChangeBoxedPoint接口,这个转型造成对p中的值进行装箱,然后在已装箱的值上调用Change方法,这是确实会将字段改成4,4但是方法返回后,已装箱的对象会被垃圾回收器回收,得到结果是2,2。
最后附上结果: