.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