为什么有些语言需要拳击和拆箱?

这不是什么是装箱和拆箱的问题,而是为什么像Java和C#这样的语言需要呢?

我非常熟悉C ++,STL和Boost。

在C ++中,我可以很容易地写出这样的东西,

std::vector dummy; 

我有一些Java经验,但我真的很惊讶,因为我必须写这样的东西,

 ArrayList dummy = new ArrayList(); 

我的问题,为什么它应该是一个对象,在谈论generics时,在技术上如此难以包含原始类型?

在谈论generics时,在技术上如此难以包含原始类型?

在Java的情况下,这是因为generics的工作方式。 在Java中,generics是一种编译时技巧,可以防止将Image对象放入ArrayList 。 但是,Java的generics是通过类型擦除实现的:generics类型信息在运行时丢失。 这是出于兼容性的原因,因为在Java的生命周期中添加了generics。 这意味着,运行时, ArrayList实际上是一个ArrayList (或者更好:只是期望并在其所有方法中返回Object ArrayList ),它在您检索值时自动转换为String

但是因为int不是从Object派生的,所以你不能将它放在期望(在运行时) Object的ArrayList中,也不能将Objectint 。 这意味着必须将原始int包装到一个inheritance自Object的类型中,如Integer

例如,C#的工作方式不同。 C#中的generics也在运行时强制执行,并且List不需要装箱。 只有当您尝试在像object这样的引用类型变量中存储类似int的值类型时才会发生C#中的Boxing。 由于C#中的intinheritance自C#中的Object ,因此写入object obj = 2是完全有效的,但是int将被装箱,这由编译器自动完成(没有Integer引用类型向用户或任何东西公开)。

拳击和拆箱是必不可少的,因为语言(如C#和Java)实现了他们的内存分配策略。

某些类型在堆栈上分配,而其他类型在堆上分配。 为了将堆栈分配的类型视为堆分配类型,需要装箱以将堆栈分配的类型移动到堆上。 拆箱是相反的过程。

在C#堆栈分配的类型中称为值类型 (例如System.Int32System.DateTime ),堆分配的类型称为引用类型 (例如System.StreamSystem.String )。

在某些情况下,能够将值类型视为参考类型(reflection是一个示例)是有利的,但在大多数情况下,最好避免装箱和拆箱。

我相信这也是因为原语不会从Objectinheritance。 假设你有一个方法想要能够接受任何东西作为参数,例如。

 class Printer { public void print(Object o) { ... } } 

您可能需要将简单的原始值传递给该方法,例如:

 printer.print(5); 

你可以在没有装箱/拆箱的情况下做到这一点,因为5是基元而不是对象。 您可以为每种基本类型重载print方法以启用此类function,但这很痛苦。

我只能告诉你Java为什么它不支持generics中的原始类型。

首先出现的问题是,如果java甚至应该有原始类型,那么每次支持这个问题都会引发讨论。 这当然阻碍了对实际问题的讨论。

其次,不包括它的主要原因是它们需要二进制向后兼容性,因此它将在不知道generics的VM上未经修改地运行。 这种向后兼容性/迁移兼容性的原因也是为什么现在Collections API支持generics并保持不变,并且没有(如在C#中引入generics时)一组全新的通用感知Collection API。

兼容性是使用ersure(在编译时删除generics类型参数信息)完成的,这也是你在java中获得如此多的未经检查的强制转换警告的原因。

您仍然可以添加具体化的generics,但这并不容易。 只是添加类型信息添加运行时而不是删除它将无法工作,因为它打破了源和二进制兼容性(你不能继续使用原始类型,你不能调用现有的编译代码,因为他们没有相应的方法)。

另一种方法是C#选择的方法:见上文

此用例不支持自动自动装箱/拆箱,因为自动装箱成本太高。

Java理论与实践:generics陷阱

在Java和C#中(与C ++不同),一切都扩展了Object,因此像ArrayList这样的集合类可以保存Object或其任何后代(基本上是任何东西)。

但是,出于性能原因,java中的原语或C#中的值类型具有特殊状态。 他们不是对象。 你不能做(在Java中):

  7.toString() 

尽管toString是Object上的一个方法。 为了将这种点头与性能联系起来,创建了等效的对象。 AutoBoxing删除了必须在其包装类中放置原语并再次将其取出的样板代码,使代码更具可读性。

C#中值类型和对象之间的差异更加灰色。 在这里看看他们是如何不同的。

存储在堆上的每个非数组非字符串对象都包含一个8或16字节的标头(32/64位系统的大小),后跟该对象的公共和私有字段的内容。 数组和字符串具有上面的标题,加上一些更多的字节定义数组的长度和每个元素的大小(可能还有维度的数量,每个额外维度的长度等),然后是第一个字段的所有字段元素,然后是第二个的所有字段等。给定对象的引用,系统可以轻松地检查标题并确定它是什么类型。

引用类型存储位置包含四个或八个字节的值,该值唯一地标识存储在堆上的对象。 在当前实现中,该值是指针,但是将其视为“对象ID”更容易(并且在语义上等效)。

值类型存储位置保存值类型字段的内容,但没有任何关联的标头。 如果代码声明了Int32类型的变量,则无需使用该Int32存储信息来说明它是什么。 该位置保存Int32的事实被有效地存储为程序的一部分,因此它不必存储在该位置本身。 如果例如一个具有一百万个对象,每个对象具有Int32类型的字段,则这代表了大量节省。 持有Int32每个对象都有一个标题,用于标识可以操作它的类。 由于该类代码的一个副本可以在百万个实例中的任何一个上运行,因此该字段是Int32是代码的一部分,这比使这些字段中的每个字段的存储都包含有关它的内容的信息要高效得多。 。

当请求将值类型存储位置的内容传递给不知道期望该特定值类型的代码时,必须进行装箱。 期望未知类型的对象的代码可以接受对存储在堆上的对象的引用。 由于存储在堆上的每个对象都有一个标识标识它是什么类型的对象的标题,因此只要需要以一种需要知道其类型的方式使用对象,代码就可以使用该标题。

请注意,在.net中,可以声明所谓的generics类和方法。 每个这样的声明自动生成一系列类或方法,除了他们希望作用的对象类型之外,它们是相同的。 如果将Int32传递给例程DoSomething(T param) ,那么它将自动生成一个例程的版本,其中类型T每个实例都被Int32有效替换。 该例程的例程将知道声明为类型T每个存储位置都包含Int32 ,因此就像例程被硬编码为使用Int32存储位置的情况一样,没有必要存储类型信息与那些地点本身。