首页 文章

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

提问于
浏览
1

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

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

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

enter image description here

这引导我到 wrong conclusion 拳击是从堆栈移动到堆的过程,其中值 - >引用转换发生(反之亦然) .

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

1. How does it looks in terms of memory schematics when boxing/unboxing happens with instance variables/class field?

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

2. What happens here, for example:

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

3. If boxing/unboxing considered "bad" in terms of memory/performance - how do you handle it in cases where you cant do that? For example:

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

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

1 回答

  • 2

    原始答案

    拳击/拆箱不会移动到堆中,也不会移动到堆中,而是关于间接 . 当变量被装箱时,你得到的是一个新对象(好的,就是在堆中,这是一个实现细节),它有一个值的副本 .

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

    [*]:例如,您将调用一个方法(或运算符),该方法将从堆栈中读取其参数(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,我将重温该代码 .

    在深入研究代码之前,我想提一下我使用“堆栈”这个词,因为 - 正如我在原始答案中所说的那样 - 这就是语言的语义 . 然而,在实践中它不一定是文字堆栈 . 抖动很可能会优化代码以利用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 .

    用户看到"1" .


    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?
    

    已在此范围中定义名为“i”的局部变量

    我想你的意思是:

    i = (int)o; // unboxing?
    

    是的,取消装箱 .

    • 推送堆栈顶部的局部变量 o 的值 .

    • Unbox :读取我们从堆栈中弹出的对象的值,并在堆栈上推送该值 .

    • 将局部变量 i 设置为我们从堆栈弹出的内容 .


    int k = (int)o; // Same?
    

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


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

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

    1. Use generics

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

    我必须承认 . 有时使用泛型不是答案 .

    2. Use ref

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

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

    3. Keep an eye on box lifespan

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

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

    4. Try using reference types

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

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

    5. Let it be

    如果没有拳击你就做不到,没有拳击就无法做到 .

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

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

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

相关问题