C# 指南之裝箱與拆箱

知識點  

  1. 值類型。
    1. 值類型是在棧中分配內存,在聲明時初始化才能使用,不能爲null。
    2. 值類型超出作用範圍系統自動釋放內存。
    3. 主要由兩類組成:結構,枚舉(enum),結構分爲以下幾類:
      1. 整型(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)
      2. 浮點型(Float、Double)
      3. decimal
      4. bool
      5. 用戶定義的結構(struct)
  2. 引用類型。
    1. 引用類型在堆中分配內存,初始化時默認爲null。
    2. 引用類型是通過垃圾回收機制進行回收。
    3. 包括類、接口、委託、數組以及內置引用類型object與string。

概念

由於C#中所有的數據類型都是由基類System.Object繼承而來的,所以值類型和引用類型的值可以通過顯式(或隱式)操作相互轉換,而這轉換過程也就是裝箱(boxing)和拆箱(unboxing)過程。

  1. 裝箱 是值類型到 object 類型或到此值類型所實現的任何接口類型的隱式轉換。對值類型裝箱會在堆中分配一個對象實例,並將該值複製到新的對象中。 
    •  
  2. 拆箱 (取消裝箱)是從 object 類型到值類型或從接口類型到實現該接口的值類型的 顯式 轉換。取消裝箱操作包括:
    1. 檢查對象實例,確保它是給定值類型的一個裝箱值。(拆箱後沒有轉成原類型,編譯時不會出錯,但運行會出錯,所以一定要確保這一點。用GetType().ToString()判斷時一定要使用類型全稱,如:System.String 而不要用String。)

    2. 將該值從實例複製到值類型變量中。

示例

首先寫個簡單的控制檯程序:

// Tutorial_boxing_unboxing.cs
// 裝箱與拆箱
using System;

class App
{
    static void Main()
    {
        int i = 32;
        object o = i; //隱式裝箱

         Console.WriteLine("o = {0}", o);

        Console.Read();
    }
}

其中object o = i這裏我們進行了裝箱操作,然後我們用MSIL 反彙編程序查看下生成的.exe程序的內部機理。

 1 .method private hidebysig static void  Main() cil managed
 2 {
 3   .entrypoint
 4   // 代碼大小       30 (0x1e)
 5   .maxstack  2
 6   .locals init ([0] int32 i,
 7            [1] object o)
 8   IL_0000:  nop
 9   IL_0001:  ldc.i4.s   32
10   IL_0003:  stloc.0
11   IL_0004:  ldloc.0
12   IL_0005:  box        [mscorlib]System.Int32
13   IL_000a:  stloc.1
14   IL_000b:  ldstr      "o = {0}"
15   IL_0010:  ldloc.1
16   IL_0011:  call       void [mscorlib]System.Console::WriteLine(string,
17                                                                 object)
18   IL_0016:  nop
19   IL_0017:  call       int32 [mscorlib]System.Console::Read()
20   IL_001c:  pop
21   IL_001d:  ret
22 } // end of method App::Main

其中第12行是我們的裝箱操作。(關於IL中出現的操作符代表的操作請查閱MSDN Library中的.NET開發/.NET Framework SDK/類庫參考/System.Reflection.Emit/OpCodes 類/OpCodes 字段)

然後我們取消裝箱操作:

    static void Main()
    {
        int i = 32;

        Console.WriteLine("i = {0}", i);

        Console.Read();
    }

再用MSIL工具查看生成的.exe,如下結果:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // 代碼大小       28 (0x1c)
  .maxstack  2
  .locals init ([0] int32 i)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   32
  IL_0003:  stloc.0
  IL_0004:  ldstr      "i = {0}"
  IL_0009:  ldloc.0
  IL_000a:  box        [mscorlib]System.Int32
  IL_000f:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_0014:  nop
  IL_0015:  call       int32 [mscorlib]System.Console::Read()
  IL_001a:  pop
  IL_001b:  ret
} // end of method App::Main

在IL_000a行,我們發現這裏卻也出現了一個box!不過這步是在call System.Console::WriteLine(string, object)時發生的。我們對比前面我們手動boxing的IL代碼,發現在我們手動boxing後就沒有這步box了。爲什麼呢?

當我們在調用一些方法的重載版本時,由於編譯器找不到符合給定參數類型的重載方法,此時編譯器便去尋找到的最接近的版本,然後使用找到的方法,而其參數卻是我們傳入的值類型的基類如System.Object或者其實現的接口類型,接着編譯器爲了求得與這個方法的原型一致,就必須對該值類型進行裝箱操作(轉換成引用類型)。

照這個說法當我們不手動boxing時,在調用了Console.WriteLine()方法輸出一個Int32類型值時,系統就要自動進行boxing。也就是說如果我們要對該輸出操作作5000次的循環,系統就要做5000次的boxing。這樣對性能便會有一定的影響,而且要使循環次數是100,000,000次呢,或者跟多!

此時我們便要想如何消除這不應該的性能損失!正如第一個程序是展示的,我們可以在需要的地方先進行boxing,這個原理很簡單,我們可以聯想到類似的做法:

//當我們如下時:
for (int i = 0; i < arr.Length; i++)
{
   // 
}

//我們更因該這樣:
int L = arr.Length;
for (int i = 0; i < L; i++)
{
   // 
}

這樣,我們只要一次boxing,就可以避免讓系統重複的做這個操作。

用途

像在調用Console.WriteLine()的過程中系統自動進行boxing一樣,當我們在調用其它的一些方法的重載版本進行操所時,爲了避免由於無謂的隱式裝箱所造成的性能損失,在執行這些多類型重載方法之前,最好先對值進行裝箱。一般是在處理大量數據需要對類型進行裝箱操作。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章