談到裝箱拆箱,DebugLZQ相信給位園子裡的博友一定可以娓娓道來,大概的意思就是實值型別和參考型別的相互轉換唄---實值型別到參考型別叫裝箱,反之則叫拆箱。這當然沒有問題,可是你只知道這麼多,那麼DebugLZQ建議你花點時間看看樓主這篇文章,繼續前幾篇博文的風格--淺談雜侃。
1. .NET中的類型
為了說明裝箱和拆箱,那首先必須先說類型。在.NET中,我們知道System.Object類型是所有內建類型的基類。注意這裡說的是內建類型,程式員可以編寫不繼承子自System.Object的類型,這裡不做過多的介紹(感興趣的博友可以研究一下)。
所有.NET的類型都可以分為兩類(有點不嚴謹,但是大家都這麼講):實值型別和參考型別。那麼實值型別和參考型別如何區分,標準是什嗎?最簡單也最明確的一個區分標準是:所有的實值型別都繼承自System.ValueType(System.ValueType繼承自System.Object),也就是說,所有繼承自System.ValueType的類型都是實值型別,而其他類型都是參考型別。(題外話:以前在讀一位博友王濤的《你必須知道的.NET》中,他說,實值型別和參考型別最本質的區別是:實值型別和參考型別在記憶體中分配的位置不同,前者分配在堆棧上,後者分配在堆上。個人覺得這個不是一個簡單明確的區分方法。遠沒有DebugLZQ說的這麼露骨!)
說到這裡,你應該要有這樣的想法:嚴格來說的話,System.Object作為所有內建類型的基類,本身並沒有實值型別和參考型別之分。但是System.Object的對象,具有參考型別的特點。這也是實值型別在有些場合需要裝箱拆箱的原因。
下面還是簡單說下實值型別和參考型別的不一樣的地方吧,分3塊,個人覺得理解這3塊就可以了:
- 變數賦值 實值型別的變數將直接獲得一個真實的資料副本,而對參考型別的賦值僅僅是吧對象的引用賦給變數,這樣就可能導致多個變數引用到一個實際對象執行個體上(這裡需要各位博友去理解.NET對String的一些最佳化機制,本質和這個不相悖)。
- 記憶體配置 參考型別的對象將在堆上分配記憶體,而實值型別的對象則會在堆棧上分配記憶體。(記憶體如何分配:堆棧上存的是什麼?實值型別變數和參考型別變數的引用。堆上存的是什嗎?參考型別的對象(包括了類型對象指標和同步塊索引,注意只是個索引,這是.NET為線程同步提出的一種折中的辦法。))。大對象堆(也是堆,一種特別的堆)什麼的這裡不做介紹。但必須說明的是:堆棧的空間有限,但運行效率卻比堆要高得多!!!
- 由於所有的實值型別都繼承自System.ValueType,而System.ValueType繼承自System.Object,並重新實現了基類System.Object的一個虛方法Equals,而參考型別並沒有重寫。
2.裝箱拆箱原理
前面簡單介紹了.NET中的類型,下面引入裝箱和拆箱。通過1我們知道實值型別的對象是在堆棧上分配記憶體的,而參考型別(包括System.Object)對象是在堆上分配記憶體的,那麼當實值型別被類型轉換時,會在堆棧和堆上進行一系列的操作,這就是裝箱拆箱的來源。
充分理解裝箱拆箱的原理,有助於我們程式員寫出高效的代碼。
梳理下:前面DebugLZQ說到,所有實值型別都繼承自System.ValueType,而Sytem.ValueType繼承自System.Object;所有實值型別對象都分配在堆棧上,而所有參考型別,當然包括System.Object,對象都分配在堆上。那麼,問題來了:既然System.Object 是所有實值型別的基類,那麼所有實值型別必然可以隱式轉換成System.Object(物件導向中的類型替換原則,基類能夠替換子類),那麼這個對象將被分配在哪裡,堆上還是堆棧上?事實上,當這個轉換髮生時,CLR需要做額外的工作把堆棧上的實值型別移動到堆上,這個操作就是被我們稱作的“裝箱”。
裝箱(box)的詳細步驟:
- 在堆上分配一個記憶體空間,大小等於需要裝箱的實值型別對象的大小加上兩個參考型別對象都擁有的成員:類型對象指標和同步區塊引述。
- 把堆棧上的實值型別對象複製到堆上新分配的對象。
- 返回一個指向堆上新對象的引用,並且儲存到堆棧上被裝箱的那個實值型別的對象裡。
這個步驟不需要程式員自己編寫,在任何出現裝箱的地方,編譯器會自動加上執行以上功能的IL代碼。
所謂的拆箱就是裝箱對應著的概念,但拆箱的過程和裝箱並不是倒過來就是:
拆箱(unbox.any)的詳細步驟
如果為待拆箱對象為null,拋出NullReferenceException異常。
如果引用指向的不是一個期望對象的已裝箱對象,拋出InvalidCastException異常。
- 擷取已裝箱對象中各個欄位的地址,這個過程就是“拆箱”
需要說明的是一般拆箱以後會伴隨著對象的拷貝,但拷貝操作已經不是拆箱的範疇。
裝箱拆箱新能比較
瞭解了裝箱和拆箱的操作,我們可以清楚的明白:裝箱操作會導致資料在堆和棧上進行拷貝,頻繁的裝箱操作會效能損失。而相比而言拆箱過程對效能損耗還是比較小的。
3 小結
裝箱和拆箱意味著堆和堆棧空間的一系列操作,毫無疑問,這些操作的效能代價是很大的,尤其是對於堆上空間的操作,速度相對於堆棧慢得多,並且可能引發記憶體回收,這些都將大規模的影響系統系能。
裝箱和拆箱操作經常發生在以下連個場合:
- 實值型別的格式化輸出
- System.Object類型的容器
第一種情況,類型的格式化輸出往往伴隨一次裝箱操作,譬如:
using System;namespace MaxValueTest{ /// <summary> /// DebugLZQ /// http://www.cnblogs.com/DebugLZQ /// </summary> class Program { static void Main(string[] args) { int i = Int32.MaxValue; Console.WriteLine("Int32的最大值是"+i);//引發了一次不必要的裝箱操作 Console.WriteLine("Int32的最大值是" + i.ToString());//ok Console.ReadKey(); } }}
第二種情況更為常見一些,例如常用的容器ArrayList,就是一個典型的System.Object容器,任何實值型別被放入到ArrayList的對象中,都會發生一次裝箱操作,而對應的取出實值型別對象會引發一次拆箱操作。
在.NET 2.0以後,引入了“泛型”的概念後,這些問題得到了有效解決。泛型允許定義針對某個特定類型(包括實值型別)的容器,並且避免裝箱和拆箱。
關於泛型的機制和原理,請關注DebugLZQ後面的博文:《淺談.NET中的泛型的機制和原理》,請期待~
請點擊下面的綠色通道---關注DebugLZQ,共同交流進步~