问题描述
假设我们有以下 C# 代码:
public static void Main()
{
int v = 5;
Object o = v;
v = 123;
Console.WriteLine(v + (Int32) o); // displays "1235"
}
.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)
我们可以看到第一次装箱和第二次装箱如下:
所以看起来当 Box
被调用时,需要的“参数”是指向 v
的第一个字段的堆栈指针。
第三个拳击的工作原理如下:
所以现在看起来当 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 是正确的。你混淆了 int32
和 unbox
来自 ECMA-335 规范(.NET 和 CIL 的规范)
第 III.4.33 部分:
与 unbox 指令不同,对于值类型,unbox.any 在堆栈上留下一个值,而不是一个值的地址。
顺便说一下,有些指令采用 unbox.any
或实际值。例如,ref valuetype
(查看有关此 here 的更多信息)
此外,你说:
所以看起来当 box 被调用时,需要的“参数”是指向 v 的第一个字段的堆栈指针。
这不是真的:ldfld
会将实际值加载到堆栈中。