I this MSDN Magazine article,作者陈述(强调我的):
请注意,拳击始终会创建一个新的 对象并复制未装箱的值 对象的位。 另一方面, 取消装箱只是返回一个指针 盒装对象中的数据:没有 发生内存复制。但是,确实如此 通常情况下你的代码会 导致数据指向的 无论如何都要复制未装箱的参考资料。
我对我加粗的句子和随后的句子感到困惑。从我读过的其他内容,包括this MSDN page,我以前从未听说过拆箱只返回指向堆上值的指针。我的印象是,拆箱会导致你有一个包含堆栈值的副本的变量,就像你开始一样。毕竟,如果我的变量包含“指向堆上的值的指针”,那么我没有值类型,我有一个指针。
有人可以解释这意味着什么吗?作者是破解? (文章中至少还有一个明显的错误)。如果这是真的,那么“你的代码将导致未装箱的引用指向的数据无论如何被复制”的情况是什么?
我刚才注意到这篇文章已有近10年的历史了,所以也许这在.Net的生命早期发生了变化。
答案 0 :(得分:7)
文章准确无误。然而,它讨论了真正的内容,而不是编译器生成的IL的样子。毕竟,.NET程序从不执行IL,它执行由JIT编译器从IL生成的机器代码。
unbox操作码确实会生成代码,该代码生成指向堆上表示值类型值的位的指针。 JIT生成对CLR中名为“JIT_Unbox”的小辅助函数的调用。 clr \ src \ vm \ jithelpers.cpp如果你有SSCLI20源代码。 Object :: GetData()函数返回指针。
从那里,最常见的值首先被复制到CPU寄存器中。然后可以存储在某处。它不必是堆栈,它可以是引用类型对象(gc堆)的成员。或者是静态变量(加载器堆)。或者它可以被推入堆栈(方法调用)。或者,当在表达式中使用该值时,可以按原样使用CPU寄存器。
在调试时,右键单击编辑器窗口并选择“转到反汇编”以查看机器代码。
答案 1 :(得分:5)
原始文章的作者必须指的是在IL级别上发生的事情。存在两个取消装箱操作码:unbox
和unbox.any
。
根据MSDN,regarding unbox.any
:
当应用于盒装形式的时候 值类型,unbox.any指令 提取其中包含的值 obj(O型),因此 相当于unbox后跟ldobj。
[...] unbox不需要复制 来自对象的值类型。 通常它只是计算 值的类型的地址 已经存在于盒装内部 对象
所以,作者知道他在说什么。
关于unbox
的这个小事实使得在直接使用IL时可以进行某些漂亮的优化。例如,如果你有一个盒装的int,你需要传递给一个接受ref int的函数,你可以只发出一个unbox
操作码,并且对函数的堆栈中就会准备对int的引用经营。在这种情况下,该函数将改变装箱对象的实际内容,这在C#级别是非常不可能的。它可以节省您为临时局部变量分配空间的需要,在那里取消int,将ref传递给函数的int,然后创建一个新的装箱对象来重新封装int,丢弃旧盒子。
当然,当你在C#级别工作时,你不能做任何这样的优化,所以通常会发生的事情是编译器生成的代码几乎总是在制作前从盒装对象复制变量任何进一步使用它。
答案 2 :(得分:1)
Boxing是将值类型实例强制转换为引用类型实例(object
或接口)的行为,并且在堆上分配引用类型。
根据'Nutshell中的C#4.0':“...取消装箱将对象的内容复制回一个值类型实例”,这意味着在堆栈上。
在您引用的文章中,作者声明:
public static void Main() {
Int32 v = 5; // Create an unboxed value type variable
Object o = v; // o refers to a boxed version of v
v = 123; // Changes the unboxed value to 123
Console.WriteLine(v + ", " + (Int32) o); // Displays "123, 5"
}
从这段代码中,你能猜出有多少 拳击行动发生?你可能是 惊讶地发现答案 是三个!我们来分析代码吧 仔细地真正了解什么是 继续 首先,创建Int32未装箱的值类型(v)并初始化为 5.然后创建一个Object引用类型(o),它想要指向v。 但引用类型必须始终指向 到堆中的对象,所以C# 生成适当的IL代码到框v 并存储盒装的地址 版本的v in o。现在123是未装箱的 并将引用的数据复制到 未装箱的值类型v;这没有 对盒装版v的影响,所以 盒装版本保持其价值 5.请注意,此示例显示了o是如何取消装箱的(它返回指向的指针) o)中的数据,然后是o中的数据 将内存复制到未装箱的值 输入v。