Boxing 是種隱含的處理,當 Value Type 物件塞到 Reference Type 時發生,會幫我們在 Managed Heap 建立一塊空間,並將本來 Value Type 的值賦予其中。

舉個例子來說,像是這邊宣告個 int 變數 i,若我們像下面這樣將它塞到 object。

int i = 123;
object o = i; // explicit boxing

{% img /images/posts/BoxingUnBoxing/1.png %}

因為 int 為 Value Type,object 為 Reference Type,故會做 Boxing 的處理。

UnBoxing 是種明確的處理,當我們將裝箱的物件明確轉型時發生,會把裝箱的物件塞回到 Stack。

像是延續之前的例子,我們將 o 轉型成 int 時,裝箱在裡面的資料會被拆箱出來放到新的 Stack 位置。

int i = 123; // a value type
object o = i; // boxing
int j = (int)o; // unboxing

{% img /images/posts/BoxingUnBoxing/2.png %}

之所以要了解 Boxing & UnBoxing 的運作,是因為他會帶來不必要的性能耗費,透過下面這段簡單的測試就可以清楚的看出。

using System;
using System.Diagnostics;

internal class Program {
private static void Main(string[] args) {
var count = 1000000000;
Console.WriteLine(“Boxing: {0} ms",
DoTest(count, () => {
var value = 123;
object value2 = value;
}));
Console.WriteLine(“None boxing: {0} ms",
DoTest(count, () => {
var value = 123;
var value2 = value;
}));
}
static long DoTest(int count, Action action) {
var sw = Stopwatch.StartNew();
for (int i = 0; i  {
var value = 123;
var value2 = string.Format("{0}", value);
}));
Console.WriteLine("None Boxing: {0} ms",
DoTest(count, () => {
var value = 123;
var value2 = string.Format("{0}", value.ToString());
}));
}
static long DoTest(int count, Action action) {
var sw = Stopwatch.StartNew();
for (int i = 0; i < count; ++i) action();
return sw.ElapsedMilliseconds;
}
}

{% img /images/posts/BoxingUnBoxing/4.png %}

可以看到其實我們很容易就造成不必要的 Boxing,因為不能避免的還是會有些方法會要求傳入 Object 型態,像是 String.Format。如果叫用時帶入的數值不主動呼叫 ToString 讓它透過低階的 API 轉成字串,就會造成 Boxing。

除了記憶體的耗費與效能的影響外,不了解 Boxing & UnBoxing 的運作可能也會寫出不如我們所預期的程式。

像是下面這段程式將整數數值塞入了物件,接著嘗試將物件轉型為 float。

using System;

internal class Program {
private static void Main(string[] args)
{
try {
var value1 = 1;
object value2 = value1;
var value3 = (float)value2;
} catch (Exception ex) {
Console.WriteLine(ex);
}
}
}

{% img /images/posts/BoxingUnBoxing/5.png %}

但因為物件內裝的是整數數值,當用 float 轉型時會長是用 float 進行拆箱,因為無法拆箱,所以系統會發出例外。

接著看一下下面這段程式。

using System;

struct Counter
{
private int x;
public void Increment() { this.x++; }
public int Count { get { return this.x; } }
}

internal class Riddle
{
public readonly object counter = new Counter();
}

internal class Program
{
private static void Main(string[] args)
{
var riddle = new Riddle();
((Counter)riddle.counter).Increment();
Console.WriteLine(((Counter)riddle.counter).Count);
}
}

{% img /images/posts/BoxingUnBoxing/6.png %}

因為拆箱後是在新的 Stack 位置,所以第一次拆箱呼叫 Increment,與後來拆箱取 Count 的實體是不同的。

既然 Boxing & UnBoxing 那麼值得我們注意,有什麼好方法可以檢測出來嗎?以前筆者是用反組譯後看 MSIL 有無 Box 命令去偵測,後來找到 ReSharper - Heap Allocation Viewer Extension 與 Clr C# Heap Allocation Analyzer,偵測起來輕鬆了許多。