什么是装箱和拆箱,什么是权衡?

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

理想情况下,作为实际的答案,虽然链接到良好的解释欢迎。

60181 次浏览

装箱和取消装箱是将原始值转换为面向对象的包装类(装箱)的过程,或者将一个值从面向对象的包装类转换回原始值(取消装箱)的过程。

例如,在 java 中,如果您想将 int值存储在 Collection中,可能需要将其转换为 Integer(装箱) ,因为原语不能存储在 Collection中,只能存储对象。但是当你想把它从 Collection中取出来的时候,你可能想得到一个 int而不是 Integer的值,这样你就可以把它解压出来。

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

现在,最常见的讨论是在 Java 的(和其他语言的)“自动装箱/自动装箱”特性的上下文中。这是 以 java 为中心的自动装箱解释

在. Net:

通常情况下,你不能依赖一个函数将使用的变量类型,所以你需要使用一个从最小公分母扩展的对象变量。这里是 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

虽然这两个列表包含相同的信息,但是第二个列表更大,速度更慢。第二个列表中的每个值实际上都是对包含 intobject的引用。

这称为装箱,因为 int是由 object包装的。当它转换回来的 int是未装箱转换回它的价值。

对于值类型(即所有 structs) ,这个过程很慢,并且可能会使用更多的空间。

对于引用类型(即所有 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

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

来自 C # 3.0 In a Nutshell:

拳击是铸造价值的行为 类型转换为引用类型:

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

拆箱是... 相反的:

// unboxing o
object o = 9;
int x = (int)o;

.NET FCL 通用集合:

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

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

有关更多信息,请参见第16章 < em > CLR via C # (第二版)

装箱的值是 数据结构,它们是围绕 基本类型 * 的最小包装器。装箱的值通常作为指向 那堆东西上对象的指针存储。

因此,装箱的值使用更多的内存,并至少需要两次内存查找来访问: 一次是获取指针,另一次是跟随指向基元的指针。显然这不是你想要的内在循环。另一方面,装箱值通常更适合系统中的其他类型。因为它们是语言中的一流数据结构,所以它们具有其他数据结构所需的元数据和结构。

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

Java 和 Haskell 有未装箱的数组,但是它们明显不如其他集合方便。然而,当需要达到性能峰值时,为了避免装箱和取消装箱的开销,有一点不便是值得的。

* 在本讨论中,基元值是可以存储在 调用堆栈上的任何值,而不是存储为指向堆上值的指针。通常只有机器类型(int、 float 等)、 struct,有时还有静态大小的数组。.NET-land 将它们称为值类型(与引用类型相反)。Java 人称之为原始类型。Haskellion 只是说它们没有装箱。

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

警告: 一个足够高级的编译器/JIT 可以在某些情况下实际检测到一个在查看源代码时语义已装箱的值,在运行时可以安全地检测到一个未装箱的值。从本质上讲,由于出色的语言实现,您的盒子有时是免费的。

像其他任何东西一样,如果不小心使用,自动装箱可能会有问题。最典型的情况是出现 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;
}
}

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

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

值类型是: intcharstructuresenumerations。 参考类型包括: Classesinterfacesarraysstringsobjects

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

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


o=123;
i=(int)o; //Unboxing.

一个盒子的语言不可知的意思就是“一个对象包含一些其他的价值”。

从字面上看,拳击是一个将某些值放入框中的操作。更具体地说,它是一个对 创造的操作,一个包含该值的新框。装箱后,可以通过 拆箱从 box 对象访问装箱值。

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

提供框主要是努力区分副作用(通常是变异)和对象上的状态,否则用户可能看不到这些状态。

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

如果对象是共享的,而不需要频繁的变异,那么实习可以使程序在运行时更有效地使用内存,代价是:

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

可变单元,即盒子,是完善的设施,正好可以解决上面列出的第一个和第二个子弹的问题。此外,在函数式语言中,可以有用于实现赋值的不可变框。有关实例,请参见 SRFI-111

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

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

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

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

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

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

  • 是否将对象放在调用堆栈或堆上是一个实现细节,与框的实现无关。
    • 有些语言实现不维护连续存储作为调用堆栈。
    • 有些语言实现甚至不使(每个线程)激活记录成为线性堆栈。
    • 一些语言实现确实在空闲存储(“堆”)上分配堆栈,并在堆栈和堆之间来回传输帧片。
    • 这些策略与盒子无关。例如,许多 Scheme 实现都有框,其中包含不同的激活记录布局,包括上面列出的所有方法。
  • 除了技术上的不准确之外,“一切都是对象”这句话与拳击无关。
    • Python、 Ruby 和 JavaScript 都使用潜在类型(默认情况下) ,因此引用某些对象的所有标识符将计算具有相同静态类型的值。Scheme 也是。
    • 一些 JavaScript 和 Ruby 实现使用所谓的 NaN 拳击来允许内联分配一些对象。其他一些(包括 CPython)则不会。使用 NaN 装箱,普通的 double对象不需要拆箱来访问它的值,而其他一些类型的值可以装箱在主机 double对象中,而且没有对 double或装箱值的引用。使用初始指针方法,主机对象指针(如 PyObject*)的值是一个对象引用,该对象引用包含一个框,其装箱值存储在动态分配的空间中。
    • 至少在 Python 中,对象不是“一切”。它们也不被称为“盒装值”,除非你讨论的是与特定实现的互操作性。