理解 C# Box 和 Unbox 内部原理

在 C# 中如果要把一个值类型赋值给一个引用类型,会产生 box 操作。我们从 CIL 来看下面两个例子的区别。

第一个例子:

static void Main(string[] args)
{
      int n = 123;
}

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 5 (0x5)
    .maxstack 1
    .entrypoint
    .locals init (
        [0] int32
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 123
    IL_0003: stloc.0
    IL_0004: ret
} // end of method Program::Main

第二个例子:

static void Main(string[] args)
{
      object n = 123;
}

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 10 (0xa)
    .maxstack 1
    .entrypoint
    .locals init (
        [0] object
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 123
    IL_0003: box [System.Runtime]System.Int32
    IL_0008: stloc.0
    IL_0009: ret
} 

第二个例子明显有一个 box 操作,需要把 int 值类型转变成 object 类型。在 CIL 中,多了一个 IL_0003: box [System.Runtime]System.Int32 操作,其内涵就是把栈顶的 123 出栈,并在堆上创建一个 System.Int32 类型的对象,保持一个指针指向这个对象,并把栈顶的 123 拷贝到这个对象上。

我们在来看一个例子:

static void Main(string[] args)
{
        int i = 123;
        object o = i;
}

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 12 (0xc)
    .maxstack 1
    .entrypoint
    .locals init (
        [0] int32,
        [1] object
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 123
    IL_0003: stloc.0
    IL_0004: ldloc.0
    IL_0005: box [System.Runtime]System.Int32
    IL_000a: stloc.1
    IL_000b: ret
} // end of method Program::Main

内存分配如下所示:

微软文档performance-tips中表明,一个值类型的 box 操作是指针赋值操作的 20 倍开销,unbox 操作是指针赋值操作的 4 倍开销。
特别需要注意的是,大量的 box 操作会导致在堆上分配大量的内存,这是很耗 CPU 的,而这些内存又是需要 GC 来回收的。在游戏中,如果触发了 GC 的回收机制,很有可能会导致卡顿的出现。所以,我们要尽量避免 box和 unbox 操作。