.NET的装箱与拆箱内幕
装箱与拆箱是.NET中非常重要的概念。
装箱是将值类型转换成引用类型,或者是实现了接口的值类型。装箱将数据存储的空间由Thread stack转存到了Managed Heap中。凡是在Managed Heap中开辟空间,都将触发GC(垃圾回收),在Thread statck将不会触发垃圾回收。
拆箱就是将数据从Managed Heap中提取出来,并拷贝到Thread stack中。所以拆箱会形成两份数据,一分在Managed Heap中,一份在Thread Statck中。
先来看一段装箱和拆箱的代码
public static void BoxUnbox() { int i = 123; object o = i;//隐式装箱 object p=(object)i;//显式装箱 int j = (int)p;//拆箱 }
IL的代码
堆栈图
可以看到i到o、i到p进行了装箱,而且o和p的数据存储到了Managed Heap中。而p到j是拆箱,数据复制了一份到Thread Stack中。
装箱可以是显式或者隐式的,但拆箱是显式的。乍一看,装箱和拆箱是互逆的操作,但从上图中可以看到,并非如此。装箱需要在Managed Heap中开辟空间,同时在空间中必须设置相应的指针(Type object ptr)和同步块索引(Sync bolck index),之后才是将Thread Stack中的数据拷贝进去。而拆箱,只是从Managed Heap中将数据拷贝到Thread Stack中,并且紧接在相应的字段后面。所以装箱和拆箱并不是完全互逆的操作。而且从消耗上讲,拆箱的消耗会少于装箱的消耗。
我们来看一段测试代码
internal struct Point { private Int32 _x, _y; public Point(Int32 x, Int32 y) { _x = x; _y = y; } public void Change(Int32 x, Int32 y) { _x = x; _y = y; } public override String ToString() { return String.Format("({0}, {1})", _x.ToString(), _y.ToString()); } } public static void TypeTest() { Point p = new Point(1, 1); Console.WriteLine("p:"+p); p.Change(2, 2); Console.WriteLine("p:" + p); Object o = p; Console.WriteLine("o:"+o); ((Point)o).Change(3, 3); Console.WriteLine("o:" + o); }
输出结果
从结果中可以看到,p初始值为(1,1),所以第一次输出时为(1,1)。经过Change函数后,第二次输出为(2,2)。将p转换成o后,输出o为(3,3)。这都是预料之中的。
但是经过(Point)o强转,又执行了Change(3,3)之后,输出的结果并不是所期望的(3,3)。这是为什么?
我们可以通过前面的Thread Stack和Managed Heap对比图知道,在将对象o拆箱为Point的时候,会将o的数据拷贝一份到Thread Statck中,这样一来,Change(3,3)的操作只是针对Thread Statck中的,而在输出时的数据是还在Managed Heap中的o。这样一想,结果就变的理所当然了。
那如果将Point继承自一个接口呢?结果又会如何?为了与Point区别,这里将Point变成Pointex。代码如下
// Interface defining a Change method internal interface IChangeBoxedPoint { void Change(Int32 x, Int32 y); } // Point is a value type. internal struct Pointex : IChangeBoxedPoint { private Int32 _x, _y; public Pointex(Int32 x, Int32 y) { _x = x; _y = y; } public void Change(Int32 x, Int32 y) { _x = x; _y = y; } public override String ToString() { return String.Format("({0}, {1})", _x.ToString(), _y.ToString()); } } public static void TypeTestPointex() { Pointex p = new Pointex(1, 1); Console.WriteLine(p); p.Change(2, 2); Console.WriteLine(p); Object o = p; Console.WriteLine(o); ((Pointex)o).Change(3, 3); Console.WriteLine(o); // Boxes p, changes the boxed object and discards it ((IChangeBoxedPoint)p).Change(4, 4); Console.WriteLine(p); // Changes the boxed object and shows it ((IChangeBoxedPoint)o).Change(5, 5); Console.WriteLine(o); }结果如下图
从结果中可以看到,p初始值为(1,1),所以第一次输出时为(1,1)。经过Change函数后,第二次输出为(2,2)。将o强转为Pointex并执行Change(3,3),输出为(2,2)。这在上一例中已经作出了解译。那(IChangedBoxedPoint)强转p和o输出的结果为什么是(2,2)和(5,5)呢。
可以思考一下前面的Thread Statck和Managed Heap。p在经过IChangedBoxedPoint强制转换时,经过了装箱(box),在Managed Heap会开辟一个空间来储存Point的x和y,在这里执行Change(4,4)操作,同时触发了GC,在执行完Changed返回时,GC自动将这一部分空间回收。p还是原来在Thread Stack中的p。所以输出的是(2,2)。
对于IChangedBoxedPoint强制转换o时,本来也是要有一个装箱操作的,不过这里的o是object,已经是装箱过的,所以不再装箱。所以Change(5,5)会改变这里的数据,同时由于IChangedBoxedPoint执行完Change后返回时,由于使用的是o空间的数据,而o还存在着,所以生命周期并没有结束,GC也就不会回收这部分数据。所以输出的是(5,5)。
据此,我们可以思考一下下面这段代码
public static void TestWriteLine() { int i = 1; Console.WriteLine("{0},{1},{2}",i,i,i); object o = i; Console.WriteLine("{0},{1},{2}",o,o,o); }两个Console.WriteLine输出的结果是一样的,但是内部却有差异。我们来查看一下IL代码
可以看到Console.WriteLine("{0},{1},{2}",i,i,i)进行三次的装箱操作,因为Console.WriteLine这时调用的是三个obj参数的方法,见下图。i是int类型,是值类型,要转换成object,所以需要装箱操作,因为是三个obj,所以有三次装箱(box)。
再看Console.WriteLine("{0},{1},{2}",o,o,o),只进行了次装箱操作,同时Console.WriteLine这时调用的是另一个方法,见下图。这里通过对象o将i进行了一次装箱,所以后面的Console.WriteLine调用时,就不再需要装箱。
由此,可以看到,虽然输出的结果一致,但因为内部的装箱操作次数不同,可以预见,两者在性能上必然是后者优于前者。
综上,我们可以得出以下结论:
1.装箱是将值类型转换成引用类型,或者是继承了接口的值类型。如果装箱后的值类型需要改变内部的字段,需要通过接口来实现。
2.装箱时,必然会在Managed Heap中开辟相应的空间,并触发GC。
3.拆箱时,会将数据从Managed Headp中拷贝一份到Thread Stack中。
4.装箱和拆箱并不完全互逆。
5.拆箱的消耗要小于装箱的消耗。
附MSDN的说明http://msdn.microsoft.com/en-us/library/yz2be5wk.aspx
转载请注明出处http://blog.csdn.net/xxdddail/article/details/36892781
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。