【发布时间】:2017-08-01 09:23:32
【问题描述】:
正如我在 Jeffrey Richter 所写的C# via CLR 一书中看到的,在谈到装箱和拆箱/复制操作时,Richter 先生展示了一个演示:
public static void Main() {
Point p; // Point is a value type defined before
p.x = p.y = 1;
Object o = p; // Boxes p; o refers to the boxed instance
// Change Point's x field to 2
p = (Point) o; // Unboxes o AND copies fields from boxed
// instance to stack variable
p.x = 2; // Changes the state of the stack variable
o = p; // Boxes p; o refers to a new boxed instance
}
为了改变一个装箱实例的值,我们需要做一个unboxing-then-boxing-again的操作,性能不是那么好。
为了避免我对书的误解,这正是作者所说的:
但是,在 C++/CLI 等某些语言中,它们允许您将 装箱值类型而不复制字段。取消装箱返回 已装箱对象的未装箱部分的地址(忽略 对象的类型对象指针和同步块索引开销)。你可以 现在使用这个指针来操作未装箱实例的字段 (恰好在堆上的一个装箱对象中)。例如, 如果用 C++/CLI 编写,以前的代码会更有效率, 因为您可以在 已经装箱的 Point 实例。这将避免同时分配一个新的 堆上的对象并复制所有字段两次!
我的问题来了:为什么 C# 不像 C++/CLR 那样做?是否还有其他一些重要问题阻止了 C# 团队进行这种“冗余”设计并导致性能不如我们预期?这种“不自然”行为的背后是什么?
【问题讨论】:
-
如果您在类中使用
Point作为字段,它将在堆上。装箱和拆箱是一个不好的例子,无论它是否在堆上。当您将 valuetype 值存储在引用类型中时,就完成了装箱。我认为这与堆栈中的值类型有关。当你装箱时,它会将引用 (pointer) 存储在堆栈上,并将数据存储在堆上。 -
@JeroenvanLangen 首先,我认为您误解了我的问题;其次,装箱会将数据保留在堆栈上并复制它,而不仅仅是“指针”。
-
实际上,将 C++/CLI 行为称为“不自然”同样有意义。在 C# 中,最佳实践是将值类型设计和使用为不可变的,这样就不会出现改变它们的整个“效率”问题。当您不需要甚至不想要它时,没有理由添加可能很复杂的功能。使用这样的功能是否可以保持内存安全?
-
@HenkHolterman Emm... 我认为您的回答可能是正确的。但是您能否提供更详细的信息,例如给出此功能如何影响内存安全的具体示例?谢谢。
-
C++/CLI 的语法允许您引用一个typed box:
System::Int32^是System::Int32的一个装箱实例。 C# 缺少这样的语法,所以它唯一能让你做的就是在拆箱时复制值。顺便说一句,这不是什么大问题,如果您使用拳击,您已经输掉了表演之战。