9

在这篇 MSDN 杂志文章中,作者指出(强调我的):

请注意,装箱始终会创建一个新对象并将未装箱值的位复制到该对象。另一方面,拆箱只是返回一个指向装箱对象中数据的指针:不会发生内存复制。但是,通常情况下,您的代码将导致未装箱引用指向的数据无论如何都被复制。

我对我加粗的句子及其后面的句子感到困惑。从我读过的所有其他内容中,包括这个 MSDN page,我以前从未听说过拆箱只会返回一个指向堆上值的指针。我的印象是拆箱会导致您拥有一个包含堆栈上值副本的变量,就像您开始时一样。毕竟,如果我的变量包含“指向堆上值的指针”,那么我没有值类型,我有一个指针。

有人可以解释这是什么意思吗?作者是在破解吗?(文章中至少还有一个明显的错误)。如果这是真的,“您的代码将导致未装箱引用指向的数据无论如何都被复制”的情况是什么?

我刚刚注意到这篇文章已经有将近 10 年的历史了,所以这可能是 .Net 生命早期发生的变化。

4

3 回答 3

7

文章是准确的。然而,它谈论的是真正发生的事情,而不是编译器生成的 IL 的样子。毕竟,.NET 程序从不执行 IL,它执行由 JIT 编译器从 IL 生成的机器代码。

拆箱操作码确实会生成代码,该代码会生成指向堆上表示值类型值的位的指针。JIT 生成对 CLR 中名为“JIT_Unbox”的小帮助函数的调用。clr\src\vm\jithelpers.cpp 如果你有 SSCLI20 源代码。Object::GetData() 函数返回指针。

从那里,最常见的值首先被复制到 CPU 寄存器中。然后可能会存储在某个地方。它不一定是堆栈,它可以是引用类型对象(gc 堆)的成员。或者一个静态变量(加载器堆)。或者它可以被压入堆栈(方法调用)。或者,在表达式中使用值时,可以按原样使用 CPU 寄存器。

调试时,右键单击编辑器窗口并选择“Go To Disassembly”以查看机器代码。

于 2010-06-05T06:01:11.687 回答
5

原始文章的作者一定是指在 IL 级别发生的事情。存在两个拆箱操作码:unboxunbox.any.

根据 MSDN,关于unbox.any

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

以及关于unbox

[...] unbox 不需要从对象中复制值类型。通常它只是计算已存在于装箱对象内部的值类型的地址。

所以,作者知道他在说什么。

这个小事实unbox使得在直接使用 IL 时可以进行某些漂亮的优化。例如,如果你有一个装箱的 int,你需要将它传递给一个接受 ref int 的函数,你可以只发出一个unbox操作码,并且对 int 的引用将在堆栈中准备好供函数操作。在这种情况下,该函数将更改装箱对象的实际内容,这在 C# 级别是完全不可能的。它使您无需为临时局部变量分配空间,将 int 拆箱,将 ref 传递给 int 给函数,然后创建一个新的装箱对象来重新装箱 int,丢弃旧的盒子。

当然,当您在 C# 级别工作时,您无法进行任何此类优化,因此通常会发生的情况是编译器生成的代码几乎总是会在进一步使用之前从装箱对象中复制变量其中。

于 2011-05-31T04:16:07.797 回答
1

装箱是将值类型实例转换为引用类型实例(一个object或一个接口)的行为,并且引用类型在堆上分配。

根据'C# 4.0 in a Nutshell':“...拆箱将对象的内容复制回值类型实例”,这意味着堆栈。

在您引用的文章中,作者指出:

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 的装箱版本的地址存储在 o 中。现在 123 被拆箱,并将引用的数据复制到拆箱的值类型 v 中;这对 v 的装箱版本没有影响,因此装箱版本保持其值 5。请注意,此示例显示了 o 是如何取消装箱的(它返回指向 o 中数据的指针),然后 o 中的数据被内存复制到未装箱的值类型 v 中。

于 2010-06-05T01:10:33.237 回答