在局部范围上下文之外的装箱/拆箱

时间:2017-11-18 08:39:24

标签: c# boxing

我花了很长时间才明白装箱/拆箱不是将变量[的值]从堆栈复制到堆的过程,而只是在值< - >引用之间进行转换的过程。所有这些都是因为我看到的所有例子都是:

int i = 12;
object o = i;
int j = (int)o;

伴随着可怕的图表(在许多不同的例子中,我看到它们是相同的),看起来像这样:

enter image description here

这导致我错误的结论,拳击是从堆栈移动到堆的过程,其中值 - >参考转换发生(反之亦然)。

现在我只了解转换过程本身,但我需要深入帮助的细微差别:

1。当使用实例变量/类字段进行装箱/拆箱时,它的内存原理图如何?

默认情况下,所有这些变量都已在堆中分配。在这个范围内拳击的任何例子,它是如何表现的?如果你不想要,不需要画画,书面解释也可以。

2。这里会发生什么,例如:

int i = 12;
object o = 12; // boxing? if so - why?
int i = (int)o; // unboxing?
int k = (int)o; // Same?

第3。如果装箱/拆箱在内存/性能方面被认为是“糟糕的” - 在你不能这样做的情况下如何处理呢?例如:

int i = 10;
ArrayList arrlst = new ArrayList();
arrlst.Add(i);
int j = (int)arrlst[0];

除了“使用泛型”之外,这里的解决方案是什么(例如,不适用的情况)。

1 个答案:

答案 0 :(得分:2)

原始答案

Boxing / Unboxing不会进出堆,而是进入间接。当变量被装箱时,你得到的是一个新对象(好的,就是在堆中,是一个实现细节),它有一个值的副本。

现在,你拿一个对象并阅读其中一个字段......会发生什么?你得到一个价值。 实现细节是它被加载到堆栈中 [*]你得到的值可以装箱(你可以创建一个新的对象来保存它的引用)。

[*]:例如,您可以调用一个方法(或运算符),它将从堆栈中读取其参数(MSIL中的语义是堆栈操作)。

顺便说一下,当你拿到田地并装箱时,盒子里的东西就是副本。 想一想,你装的是来自堆栈的(你首先将它从堆复制到堆栈,然后将其打包。至少这是MSIL中的语义)。示例:

void Main()
{
    var t = new test();
    t.boxme = 1;
    object box = t.boxme;
    t.boxme = 2;
    Console.WriteLine(box); // outputs 1
}

class test
{
    public int boxme;
}

在LINQPad上测试。

扩展答案

在这里,我将回顾编辑问题中的要点......

  

<强> 1。当使用实例变量/类字段进行装箱/拆箱时,它的内存原理图如何?

     

默认情况下,所有这些变量都已在堆中分配。在这个范围内拳击的任何例子,它是如何表现的?如果你不想要,不需要画画,书面解释也可以。

我想让你解释拳击如何在实例字段上运行。由于上面的代码演示了在实例字段中使用box,我将查看该代码。

在深入研究代码之前,我想提一下我使用&#34; stack&#34;因为 - 正如我在原始答案中所说的那样 - 这就是语言的语义。然而,在实践中它不一定是文字堆栈。抖动很可能会优化代码以利用CPU寄存器。因此,当你看到我说我们把东西放在堆栈中立即将它们取出时...是的,抖动可能会在那里使用寄存器。实际上,我们会反复将一些东西放在堆栈上;抖动可能决定为这些事情重用寄存器是值得的。

首先,我们使用的是一个非常简单,不实用的class test,只有一个字段boxme

class test
{
    public int boxme;
}

关于这个类我唯一要说的是提醒你编译器会生成一个没有参数的构造函数。考虑到这一点,让我们一行一行地查看Main中的代码......

var t = new test();

这一行有两个操作:

  • 调用类test的构造函数。它将在堆上创建一个新对象,并在堆栈上推送对它的引用。
  • 将局部变量t设置为我们从堆栈中弹出的内容。
t.boxme = 1;

这一行做了三个操作:

  • 将局部变量t的值推到堆栈顶部。
  • 将值1推到堆栈顶部。
  • 将字段boxme设置为从我们从堆栈中弹出引用的对象的堆栈(1)中弹出的值。
object box = t.boxme;

正如您可能猜到的那样,这条线就是我们在这里的目的。它总共进行了四次操作:

  • 将局部变量t的值推到堆栈顶部。
  • 将字段boxme(从堆栈中弹出引用的对象)的值推送到堆栈顶部。
  • BOX :从堆栈弹出,将值(以及它是int的事实)复制到新对象(在堆中创建),推送对它的引用在堆栈上。
  • 将局部变量box设置为我们从堆栈中弹出的内容。
t.boxme = 2;

t.boxme = 1;基本相同,但我们会推送2而不是1

Console.WriteLine(box);
  • 将局部变量box的值推到堆栈顶部。
  • 使用从堆栈中弹出的任何内容作为参数调用方法System.Console.WriteLine

用户看到&#34; 1&#34;

  

<强> 2。这里会发生什么,例如:

int i = 12;
object o = 12; // boxing? if so - why?
int i = (int)o; // unboxing?
int k = (int)o; // Same?

是的,更多代码......

int i = 12;
  • 将值12推到堆栈顶部。
  • 将局部变量i设置为我们从堆栈中弹出的内容。

到目前为止没有惊喜。

object o = 12; // boxing? if so - why?

是的,拳击。

  • 将值12推到堆栈顶部。
  • BOX :从堆栈弹出,将值(以及它是int的事实)复制到新对象(在堆中创建),推送对它的引用在堆栈上。
  • 将局部变量o设置为我们从堆栈中弹出的内容。

为什么呢?因为使int的32位看起来不像引用类型。如果你想要一个值为int的引用类型,你需要将int的值放在某个地方它可以被引用(把它放在堆上)然后你就可以拥有object }。

int i = (int)o; // unboxing?
  

一个名为&#39; i&#39;的局部变量已在此范围内定义

我认为你的意思是:

i = (int)o; // unboxing?

是的,取消装箱。

  • 将局部变量o的值推送到堆栈顶部。
  • Unbox :读取我们从堆栈中弹出的对象的值,并将该值推送到堆栈上。
  • 将局部变量i设置为我们从堆栈中弹出的内容。
int k = (int)o; // Same?

是。只是一个不同的局部变量。

  

第3。如果拳击/拆箱被认为是&#34;坏&#34;在内存/性能方面 - 如果你不能这样做,你如何处理?例如:

int i = 10;
ArrayList arrlst = new ArrayList();
arrlst.Add(i);
int j = (int)arrlst[0];

<强> 1。使用泛型

int i = 10;
var arrlst = new List<T>();
arrlst.Add(i);
int j = arrlst[0];

我不得不承认。有时使用泛型不是答案。

<强> 2。使用ref

C#7.0有ref回复,本地人应该覆盖我们过去需要装箱/拆箱的一些情况。

通过使用ref,您传递的是对存储在堆栈中的值的引用。由于ref的想法是你可以修改原文,使用框(将值复制到堆)会违反其目的。

第3。密切关注盒子寿命

您可以尝试重复使用引用,而不是多次不必要地装入相同的值。这可能有助于保持盒子的数量很少,垃圾收集器会选择这些是长寿命的盒子并且不那么频繁地检查它们。

另一方面,垃圾收集器将非常有效地处理短期盒子。因此,如果你无法避免大量的装箱/拆箱,那么试着让箱子短暂存在。

<强> 4。尝试使用参考类型

如果你有,性能问题,因为你有许多长寿盒...你可能需要做一些课程。如果您开始使用引用类型,则无需将它们包装起来。

虽然如果你需要用于互操作的结构,这可能会有问题...嗯...可能不是你想要的,但看看ref structSpan<T>等。人。可以通过其他方式为您节省分配。

<强> 5。让它成为

如果没有拳击就无法做到,没有拳击就无法做到。

例如,如果您需要一个通用容器,对泛型类型的成员进行原子操作......但您还需要允许泛型类型为值类型......那么您做什么?好吧,当你需要存储一些非原子值类型时,你必须使用类型object初始化容器。

不,ref在这种情况下不会保存你,因为ref不保证原子性。

而不是更加努力地通过优化使用装箱/拆箱来获得性能增益......寻找其他方法来提高性能。例如,我所谈论的那个通用容器可能很昂贵,但是如果它允许你并行化某些算法并且提供的性能提升大于该成本,那么这是合理的。