144

我正在寻找一个清晰、简洁和准确的答案。

理想情况下作为实际答案,尽管欢迎提供良好解释的链接。

4

9 回答 9

207

盒装值是对原始类型*进行最小包装的数据结构。装箱值通常存储为指向堆上对象的指针。

因此,装箱的值使用更多的内存,并且至少需要两次内存查找来访问:一次获取指针,另一次跟随该指针指向原语。显然,这不是您在内部循环中想要的那种东西。另一方面,盒装值通常与系统中的其他类型更好地配合使用。由于它们是语言中的一流数据结构,因此它们具有其他数据结构所具有的预期元数据和结构。

在 Java 和 Haskell 中,泛型集合不能包含未装箱的值。.NET 中的泛型集合可以保存未装箱的值而不会受到惩罚。Java 的泛型仅用于编译时类型检查,.NET 将为在运行时实例化的每个泛型类型生成特定的类

Java 和 Haskell 有未装箱的数组,但它们明显不如其他集合方便。但是,当需要峰值性能时,避免装箱和拆箱的开销是值得的。

* 对于本次讨论,原始值是可以存储在调用堆栈上的任何值,而不是存储为指向堆上值的指针。通常这只是机器类型(整数、浮点数等)、结构,有时是静态大小的数组。.NET-land 称它们为值类型(与引用类型相反)。Java 人称它们为原始类型。Haskellions 只是称它们为未装箱的。

** 在这个答案中,我还关注 Java、Haskell 和 C#,因为这就是我所知道的。值得一提的是,Python、Ruby 和 Javascript 都有专门的装箱值。这也被称为“一切都是对象”方法***。

*** 警告:在某些情况下,足够先进的编译器/JIT 可以实际检测到在查看源代码时被语义装箱的值,在运行时可以安全地成为未装箱的值。本质上,多亏了出色的语言实现者,您的盒子有时是免费的。

于 2008-08-24T20:35:12.520 回答
130

来自C# 3.0 简而言之

装箱是将值类型转换为引用类型的行为:

int x = 9; 
object o = x; // boxing the int

拆箱是......相反:

// unboxing o
object o = 9; 
int x = (int)o; 
于 2008-08-16T08:44:40.223 回答
74

装箱和拆箱是将原始值转换为面向对象的包装类(装箱)或将值从面向对象的包装类转换回原始值(拆箱)的过程。

例如,在 java 中,如果要将int值存储在 a 中,则可能需要将其转换为Integer(装箱),Collection因为原语不能存储在 a 中Collection,只能存储在对象中。但是,当您想将其从 中取回时,Collection您可能希望将值作为 anint而不是 an,Integer因此您可以将其拆箱。

装箱和拆箱本身并不,但它是一种权衡。根据语言实现,它可能比仅使用原语更慢且更占用内存。但是,它也可能允许您使用更高级别的数据结构并在代码中实现更大的灵活性。

如今,它最常在 Java(和其他语言)的“自动装箱/自动拆箱”功能的上下文中讨论。这是一个以java 为中心的 autoboxing 解释

于 2008-08-16T08:39:41.700 回答
23

在 .Net 中:

通常,您不能依赖函数将使用的变量类型,因此您需要使用从最低公分母扩展的对象变量 - 在 .Net 中 this 是object.

然而object是一个类并将其内容存储为参考。

List<int> notBoxed = new List<int> { 1, 2, 3 };
int i = notBoxed[1]; // this is the actual value

List<object> boxed = new List<object> { 1, 2, 3 };
int j = (int) boxed[1]; // this is an object that can be 'unboxed' to an int

虽然它们都包含相同的信息,但第二个列表更大且更慢。第二个列表中的每个值实际上都是对object包含int.

这被称为装箱,因为intobject. 当它的回退时int,它被取消装箱 - 转换回它的价值。

对于值类型(即 all structs),这很慢,并且可能会使用更多空间。

对于引用类型(即 all classes),这不是什么问题,因为它们无论如何都是作为引用存储的。

装箱值类型的另一个问题是,您处理的是盒子而不是值并不明显。当您比较两个structs时,您正在比较值,但是当您比较两个时classes(默认情况下)您正在比较参考 - 即这些是同一个实例吗?

在处理装箱值类型时,这可能会令人困惑:

int a = 7;
int b = 7;

if(a == b) // Evaluates to true, because a and b have the same value

object c = (object) 7;
object d = (object) 7;

if(c == d) // Evaluates to false, because c and d are different instances

很容易解决:

if(c.Equals(d)) // Evaluates to true because it calls the underlying int's equals

if(((int) c) == ((int) d)) // Evaluates to true once the values are cast

然而,在处理装箱值时要小心另一件事。

于 2008-08-16T08:41:34.380 回答
4

Boxing是将值类型转换为引用类型的过程。而Unboxing将引用类型转换为值类型。

EX: int i = 123;
    object o = i;// Boxing
    int j = (int)o;// UnBoxing

值类型是:int,charstructures, enumerations。引用类型为 :Classesinterfacesarraysstringsobjects

于 2014-02-05T21:16:55.583 回答
2

.NET FCL 通用集合:

List<T>
Dictionary<TKey, UValue>
SortedDictionary<TKey, UValue>
Stack<T>
Queue<T>
LinkedList<T>

都是为了克服以前的集合实现中的装箱和拆箱的性能问题而设计的。

有关更多信息,请参阅第 16 章,通过 C# 进行 CLR(第 2 版)

于 2008-08-16T09:24:15.527 回答
1

装箱和拆箱有助于将值类型视为对象。装箱意味着将值转换为对象引用类型的实例。例如,Int是一个类,int是一个数据类型。转换int成是装箱Int的一个例子,而转换Intint是拆箱。这个概念有助于垃圾收集,另一方面,拆箱将对象类型转换为值类型。

int i=123;
object o=(object)i; //Boxing

o=123;
i=(int)o; //Unboxing.
于 2016-06-21T11:20:26.940 回答
1

盒子的语言无关含义只是“一个对象包含一些其他值”。

从字面上看,装箱是将一些值放入框中的操作。更具体地说,它是创建一个包含该值的新框的操作。装箱后,可以通过取消装箱从盒子对象中访问装箱后的

请注意,许多编程语言中的对象(不是特定于 OOP 的)是关于identities的,但值不是。两个对象是相同的iff。它们具有在程序语义中无法区分的身份。值也可以相同(通常在某些相等运算符下),但我们不会将它们区分为“一个”或“两个”唯一值。

提供盒子主要是为了将副作用(通常是突变)与对象的状态区分开来,否则用户可能看不到这些状态。

默认情况下,一种语言可能会限制访问对象的允许方式并隐藏对象的身份。例如,典型的 Lisp 方言在对象和值之间没有明确的区别。因此,实现可以自由地共享对象的底层存储,直到对象发生一些变异操作(因此必须在操作后将对象从共享实例中“分离”才能使效果可见,即变异存储在对象中的值可能与具有旧值的其他对象不同)。这种技术有时被称为对象实习

如果对象是共享的而不需要频繁的变异,则实习使程序在运行时的内存效率更高,代价是:

  • 用户无法区分对象的身份。
    • 在某些副作用实际发生之前,没有办法识别一个对象并确保它具有明确独立于程序中其他对象的状态(并且实现不会积极地同时进行实习;尽管这应该是罕见的情况)。
  • 在互操作方面可能存在更多问题,需要为不同的操作识别不同的对象。
  • 此类假设存在错误的风险,因此应用实习实际上会使性能变得更糟。
    • 这取决于编程范式。频繁改变对象的命令式编程肯定不适用于实习。
  • 依赖于 COW(写时复制)来确保实习的实现可能会在并发环境中导致严重的性能下降。
    • 即使是专门针对一些内部数据结构的本地共享也可能很糟糕。例如,出于这个原因,ISO C++ 11 不允许共享内部元素std::basic_string,即使以破坏至少一个主流实现 (libstdc++) 上的 ABI 为代价。
  • 装箱和拆箱会导致性能损失。这一点很明显,尤其是当这些操作可以手动避免但实际上对优化器来说并不容易时。但是,成本的具体衡量取决于(基于每个实施甚至每个程序)。

可变单元,即盒子,是完善的设施,可以解决上面列出的第 1 和第 2 个问题。此外,可以有不可变的盒子来实现函数式语言中的赋值。实际例子见SRFI-111

使用可变单元格作为按值调用策略的函数参数实现了在调用者和被调用者之间共享突变的可见效果。从这个意义上说,盒子包含的对象实际上是“被共享调用的”。

有时,这些框被称为引用(这在技术上是错误的),因此共享语义被命名为“引用语义”。这是不正确的,因为并非所有引用都可以传播可见的副作用(例如不可变引用)。引用更多地是关于通过间接公开访问,而框是努力公开访问的最小细节,例如是否间接(这是不感兴趣的,最好通过实现避免)。

此外,“价值语义”在这里是无关紧要的。值不反对引用,也不反对框。上面所有的讨论都是基于价值调用策略。对于其他人(如按名称调用或按需要调用),不需要框来以这种方式共享对象内容。

Java 可能是第一个使这些特性在业界流行的编程语言。不幸的是,这个话题似乎有很多不好的后果:

  • 整体编程范式不适合设计。
  • 实际上,实习仅限于特定对象,如不可变字符串,并且经常指责(自动)装箱和拆箱的成本。
  • 基本的 PL 知识,如语言规范中术语“对象”(作为“类的实例”)的定义,以及参数传递的描述,与原始的、众所周知的含义相比,在程序员采用Java。
    • 至少 CLR 语言遵循类似的说法。

有关实现的更多提示(以及对此答案的评论):

  • 将对象放在调用栈还是堆上是一个实现细节,与盒子的实现无关。
    • 一些语言实现不维护一个连续的存储作为调用堆栈。
    • 某些语言实现甚至不使(每个线程)激活记录成为线性堆栈。
    • 一些语言实现确实在空闲存储(“堆”)上分配堆栈,并在堆栈和堆之间来回传输帧切片。
    • 这些策略与盒子无关。例如,许多 Scheme 实现都有盒子,具有不同的激活记录布局,包括上面列出的所有方式。
  • 除了技术上的不准确之外,“一切都是对象”的说法与拳击无关。
    • Python、Ruby 和 JavaScript 都使用潜在类型(默认情况下),因此所有引用某些对象的标识符都将评估为具有相同静态类型的值。Scheme也是如此。
    • 一些 JavaScript 和 Ruby 实现使用所谓的NaN-boxing来允许内联分配一些对象。其他一些(包括CPython)没有。使用 NaN 装箱,普通double对象无需拆箱即可访问其值,而某些其他类型的值可以装箱到宿主double对象中,并且没有引用double或装箱后的值。使用朴素指针方法,宿主对象指针的值就像PyObject*是一个对象引用,它持有一个盒子,盒子的装箱值存储在动态分配的空间中。
    • 至少在 Python 中,对象不是“一切”。除非您谈论与特定实现的互操作性,否则它们也不称为“装箱值”。
于 2021-11-16T14:54:35.267 回答
-2

像其他任何事情一样,如果不小心使用,自动装箱可能会出现问题。经典的是最终得到一个 NullPointerException 并且无法追踪它。即使使用调试器。试试这个:

public class TestAutoboxNPE
{
    public static void main(String[] args)
    {
        Integer i = null;

        // .. do some other stuff and forget to initialise i

        i = addOne(i);           // Whoa! NPE!
    }

    public static int addOne(int i)
    {
        return i + 1;
    }
}
于 2008-09-19T12:59:41.143 回答