淺談.NET中的型別和裝箱/拆箱原理
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中的泛型的機制和原理》,請期待~