這個做法在討論 boxing 與 unboxing 的運作方式與背後理論。
首先要先複習 Value Type
和 Reference Type
兩者關鍵的區別,Value Type
的複製是 Copy-by-value,Reference Type
的複製是 Copy-by-reference。
建立 Reference Type
時會有以下行為會影響效能:
- 由 Managed Heap 管理及分配記憶體。
- 每個 Heap 上分配的物件會有幾個額外的 members 需要初始化。
- 物件內含的 fields 會初始化為 0。
- Managed Heap 為物件分配記憶體時可能會強制執行 GC。
試想如果 .NET 中所有物件都是 Reference Type
那麼每次使用例如 int
背後都要進行一次內存分配那效能會有多差。
因此提供了 Value Type
這個更輕量級的類型,Value Type
是由 Thread Stack 管理及分配記憶體,並且特色是
一個 Value Type
的變數儲存的是實際的值而並不是記憶體位置,因此讀取這類型的變數不需要 dereference,並且
不受 GC 管控,所以可以大大降低 Managed Heap 的壓力與 GC 的次數。
Boxing
但並不是所有方法都有多載的版本能夠接受 Value Type
,例如 string.Concat 這個把字串相連在一起的方法就是其中一個例子。
以這段程式為例子 string.Concat 一般來說傳入的參數為 string 類型,很明顯我們的 int 類型它是不會接受的。
void Main()
{
int year = 2024;
Console.WriteLine(string.Concat(year));
}
所以這裡我們有兩個選擇:
- 將呼叫 ToString 方法將 year 轉換成 string 類型。
- 將 year 轉換成 object 類型,使用 object 多載的版本。
而將 Value Type
轉換成 object 類型的操作就叫做 boxing
,從 object 轉回當初的類型就叫做 unboxing
。
可以從 IL 碼中看出 IL_0008
就是一個 boxing
的操作。
IL_0000 nop
IL_0001 ldc.i4 E8 07 00 00 // 2024
IL_0006 stloc.0 // year
IL_0007 ldloc.0 // year
IL_0008 box Int32
IL_000D call String.Concat(Object)
IL_0012 call Console.WriteLine(String)
IL_0017 nop
IL_0018 ret
當一個 Value Type
裝箱成 object 類型就正式變成一個 Reference Type
,
從上面整理的影響效能的清單中可以得出 boxing
就是一個影響效能的操作。
從 IL 碼中可以得知 Stack
目前有分配 int32 也就是 4 個 Byte 的記憶體來記錄 year 變數,
接下來 boxing
會在 Heap
中建立一個新的箱子並把 year 變數資料複製到這個箱子裡面,之後把這個箱子的參考位址記錄到 Stack
裡面。
也同樣得出 boxing
是一個影響效能的操作。
Unboxing
接下來看 unboxing
也就是把 object 轉回原本的類型,有一個需要注意的特性那就是不可以轉型到其他類型,不是原本的型別會報 InvalidCastException
錯誤。
void Main()
{
object year = (object)2024;
long nextYear = (long)year + 1;
}
unboxing
會將原本 Heap
中的資料複製回 Stack
,所以可以想像出一次裝箱拆箱會占用許多不必要使用的記憶體。
void Main()
{
object year = (object)2024;
int nextYear = (int)year + 1;
}
從 IL 碼中的 IL_000D 可以看到 unbox 的操作。
IL_0000 nop
IL_0001 ldc.i4 E8 07 00 00 // 2024
IL_0006 box Int32
IL_000B stloc.0 // year
IL_000C ldloc.0 // year
IL_000D unbox.any Int32
IL_0012 ldc.i4.1
IL_0013 add
IL_0014 stloc.1 // nextYear
IL_0015 ret
Summary
從這個做法中可以學到 Value Type
和 Reference Type
的區別還有 boxing
與 unboxing
處理的流程與它的缺點,
其中最不容易發現的就是隱含轉型的裝箱,也就是上面 string.Concat
的例子,這種背後自動轉型的很容易沒注意到就會影響到效能,
所以當不確定是否寫法中有 boxing
與 unboxing
的行為時,最好是搭配反編譯軟體查看 IL 碼才是最準確的。