1. 程式人生 > >.NET的裝箱與拆箱內幕

.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