淺談.NET中的類型和裝箱/拆箱原理


  談到裝箱拆箱,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塊就可以了:

  1. 變量賦值   值類型的變量將直接獲得一個真實的數據副本,而對引用類型的賦值僅僅是吧對象的引用賦給變量,這樣就可能導致多個變量引用到一個實際對象實例上(這里需要各位博友去理解.NET對String的一些優化機制,本質和這個不相悖)。
  2. 內存分配   引用類型的對象將在堆上分配內存,而值類型的對象則會在堆棧上分配內存。(內存如何分配:堆棧上存的是什么?值類型變量和引用類型變量的引用。堆上存的是什么?引用類型的對象(包括了類型對象指針和同步塊索引,注意只是個索引,這是.NET為線程同步提出的一種折中的辦法。))。大對象堆(也是堆,一種特別的堆)什么的這里不做介紹。但必須說明的是:堆棧的空間有限,但運行效率卻比堆要高得多!!!
  3. 由於所有的值類型都繼承自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)的詳細步驟:

  1. 在堆上分配一個內存空間,大小等於需要裝箱的值類型對象的大小加上兩個引用類型對象都擁有的成員:類型對象指針和同步塊引用。
  2. 把堆棧上的值類型對象復制到堆上新分配的對象。
  3. 返回一個指向堆上新對象的引用,並且存儲到堆棧上被裝箱的那個值類型的對象里。

  這個步驟不需要程序員自己編寫,在任何出現裝箱的地方,編譯器會自動加上執行以上功能的IL代碼。

  所謂的拆箱就是裝箱對應着的概念,但拆箱的過程和裝箱並不是倒過來就是:

  拆箱(unbox.any)的詳細步驟

  如果為待拆箱對象為null,拋出NullReferenceException異常。

  如果引用指向的不是一個期望對象的已裝箱對象,拋出InvalidCastException異常。

  1. 獲取已裝箱對象中各個字段的地址,這個過程就是“拆箱”

  需要說明的是一般拆箱以后會伴隨着對象的拷貝,但拷貝操作已經不是拆箱的范疇。

  裝箱拆箱新能比較

   了解了裝箱和拆箱的操作,我們可以清楚的明白:裝箱操作會導致數據在堆和棧上進行拷貝,頻繁的裝箱操作會性能損失。而相比而言拆箱過程對性能損耗還是比較小的。

  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,共同交流進步~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM