Box CIL 如何在 .net 内部工作?

问题描述

假设我们有以下 C# 代码

public static void Main() 
{
   int v = 5;
   Object o = v;
   v = 123;
   Console.WriteLine(v + (Int32) o); // displays "1235"
}

生成的IL代码是:

.locals init ([0]int32 v,[1] object o)

 // Load 5 into v.
 IL_0000: ldc.i4.5
 IL_0001: stloc.0

 // Box v and store the reference pointer in o.    <------first Boxing
 IL_0002: ldloc.0
 IL_0003: Box [mscorlib]system.int32
 IL_0008: stloc.1

 // Load 123 into v.
 IL_0009: ldc.i4.s 123
 IL_000b: stloc.0

 // Box v and leave the pointer on the stack for Concat.  <------second Boxing
 IL_000c: ldloc.0
 IL_000d: Box [mscorlib]system.int32

 // UnBox o: Get the pointer to the In32's field on the stack.
 IL_0017: ldloc.1
 IL_0018: unBox.any [mscorlib]system.int32

 // Box the Int32 and leave the pointer on the stack for Concat.   <------third Boxing
 IL_001d: Box [mscorlib]system.int32

 // Call Concat.
 IL_0022: call string [mscorlib]System.String::Concat(object,object) 

我们可以看到第一次装箱和第二次装箱如下:

  1. 将第一个参数 v 压入堆栈。

  2. 调用 Box CIL

所以看起来当 Box调用时,需要的“参数”是指向 v 的第一个字段的堆栈指针。

第三个拳击的工作原理如下:

  1. 前面的unBox创建了一个值类型指针,这个值类型指针指向堆上装箱实例的第一个字段,然后这个值类型指针被压入栈中。

  2. 调用 Box CIL

所以现在看起来当 Box调用时,它首先通过取消引用堆栈指针来检查堆栈指针以获取内容(指向堆的值类型指针)。

所以我的问题是,Box CIL 是否设计为通用的,有时它直接读取堆栈指针,有时它取消引用堆栈指针以获取一个指针(在我的情况下是指向堆的指针)?

解决方法

unbox.any 取消装箱并加载堆栈中的值类型(因此它复制它):

来自MSDN

结果对象引用或值类型被压入堆栈。

当应用于值类型的装箱形式时,unbox.any 指令提取包含在 obj(O 类型)中的值,因此等价于 unbox 后跟 ldobj。 >

你在想的是unbox instruction

unbox 指令将对象引用(O 类型)(值类型的装箱表示)转换为值类型指针(托管指针,类型 &),即其未装箱形式。提供的值类型 (valType) 是元数据标记,指示包含在装箱对象中的值类型的类型。

与 Box 不同,Box 需要复制值类型以用于对象,而 unbox 不需要从对象复制值类型。通常,它只是计算已存在于装箱对象内部的值类型的地址。

我什至不知道如何强制编译器使用 unbox 指令(从 here 读取它没有在 C# 中使用,或者至少它没有在 C# 中使用2010 年的编译器...我做了一些测试,混合了 ref、装箱和拆箱,但我无法强制编译器使用它)

嗯...通过查看 ILSpy(他们反编译 C# 代码的专家),似乎 unbox 仅用于“私有" 某些 switch 的实现(switch 语句根据数量和条件类型以不同方式编译)。关于 unbox 的唯一参考是在一个名为 MatchLegacySwitchOnStringWithHashtable 的方法中......我会说这个名字很清楚。另一个引用位于 Unsafe.il 文件中...该文件“链接”到 .NET 的 Unsafe 类。请参阅关于 Unsafe.Unbox<T> 方法的提案 here。该方法已被接受并且现在是 .NET 的一部分。 corresponding C# code 无法编译:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T Unbox<T>(object box) where T : struct
{
    return ref (T)box;
}

事实上,通过查看 .NET code,它可能被实现为一个内在函数。

取消一切.. Charlieface 已经找到了如何强制使用 unbox

public struct MyStruct
{
    public int A;

    public int Test()
    {
        object st2 = new MyStruct();
        int a = ((MyStruct)st2).A;
        return a;
    }
}

方法 Test() 被编译为:

// Methods
.method public hidebysig 
    instance int32 Test () cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 25 (0x19)
    .maxstack 1
    .locals init (
        [0] valuetype MyStruct
    )

    IL_0000: ldloca.s 0
    IL_0002: initobj MyStruct
    IL_0008: ldloc.0
    IL_0009: box MyStruct
    IL_000e: unbox MyStruct
    IL_0013: ldfld int32 MyStruct::A
    IL_0018: ret
} // end of method MyStruct::Test

some tests 我会说系列中的智能操作码是 ldfld:它可以同时处理值类型 (Test1)、值类型的引用 ({{1} }}) 和直接拆箱的值类型 (Test2)。

Test3

被编译为

public struct MyStruct
{
    public int A;

    public int Test1(MyStruct st)
    {
        int a = st.A;
        return a;
    }

    public int Test2(ref MyStruct st)
    {
        int a = st.A;
        return a;
    }

    public int Test3(MyStruct st)
    {
        object st2 = st;
        int a = ((MyStruct)st2).A;
        return a;
    }
}

.method public hidebysig instance int32 Test1 ( valuetype MyStruct st ) cil managed { // Method begins at RVA 0x2050 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.1 IL_0001: ldfld int32 MyStruct::A IL_0006: ret } // end of method MyStruct::Test1 .method public hidebysig instance int32 Test2 ( valuetype MyStruct& st ) cil managed { // Method begins at RVA 0x2050 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.1 IL_0001: ldfld int32 MyStruct::A IL_0006: ret } // end of method MyStruct::Test2 .method public hidebysig instance int32 Test3 ( valuetype MyStruct st ) cil managed { // Method begins at RVA 0x2058 // Code size 17 (0x11) .maxstack 8 IL_0000: ldarg.1 IL_0001: box MyStruct IL_0006: unbox MyStruct IL_000b: ldfld int32 MyStruct::A IL_0010: ret } // end of method MyStruct::Test3 操作码总是相同的,它使用两(三)种不同的类型:ldfld 和对 int32 的引用(以及对盒装 int32 ).

,

@xanatos 是正确的。你混淆了 int32unbox

来自 ECMA-335 规范(.NET 和 CIL 的规范)

第 III.4.33 部分:

与 unbox 指令不同,对于值类型,unbox.any 在堆栈上留下一个值,而不是一个值的地址。

顺便说一下,有些指令采用 unbox.any 或实际值。例如,ref valuetype(查看有关此 here 的更多信息)


此外,你说:

所以看起来当 box 被调用时,需要的“参数”是指向 v 的第一个字段的堆栈指针。

这不是真的:ldfld 会将实际值加载到堆栈中。