我正在寻找一个清晰、简洁和准确的答案。
理想情况下作为实际答案,尽管欢迎提供良好解释的链接。
盒装值是对原始类型*进行最小包装的数据结构。装箱值通常存储为指向堆上对象的指针。
因此,装箱的值使用更多的内存,并且至少需要两次内存查找来访问:一次获取指针,另一次跟随该指针指向原语。显然,这不是您在内部循环中想要的那种东西。另一方面,盒装值通常与系统中的其他类型更好地配合使用。由于它们是语言中的一流数据结构,因此它们具有其他数据结构所具有的预期元数据和结构。
在 Java 和 Haskell 中,泛型集合不能包含未装箱的值。.NET 中的泛型集合可以保存未装箱的值而不会受到惩罚。Java 的泛型仅用于编译时类型检查,.NET 将为在运行时实例化的每个泛型类型生成特定的类。
Java 和 Haskell 有未装箱的数组,但它们明显不如其他集合方便。但是,当需要峰值性能时,避免装箱和拆箱的开销是值得的。
* 对于本次讨论,原始值是可以存储在调用堆栈上的任何值,而不是存储为指向堆上值的指针。通常这只是机器类型(整数、浮点数等)、结构,有时是静态大小的数组。.NET-land 称它们为值类型(与引用类型相反)。Java 人称它们为原始类型。Haskellions 只是称它们为未装箱的。
** 在这个答案中,我还关注 Java、Haskell 和 C#,因为这就是我所知道的。值得一提的是,Python、Ruby 和 Javascript 都有专门的装箱值。这也被称为“一切都是对象”方法***。
*** 警告:在某些情况下,足够先进的编译器/JIT 可以实际检测到在查看源代码时被语义装箱的值,在运行时可以安全地成为未装箱的值。本质上,多亏了出色的语言实现者,您的盒子有时是免费的。
来自C# 3.0 简而言之:
装箱是将值类型转换为引用类型的行为:
int x = 9;
object o = x; // boxing the int
拆箱是......相反:
// unboxing o
object o = 9;
int x = (int)o;
装箱和拆箱是将原始值转换为面向对象的包装类(装箱)或将值从面向对象的包装类转换回原始值(拆箱)的过程。
例如,在 java 中,如果要将int
值存储在 a 中,则可能需要将其转换为Integer
(装箱),Collection
因为原语不能存储在 a 中Collection
,只能存储在对象中。但是,当您想将其从 中取回时,Collection
您可能希望将值作为 anint
而不是 an,Integer
因此您可以将其拆箱。
装箱和拆箱本身并不坏,但它是一种权衡。根据语言实现,它可能比仅使用原语更慢且更占用内存。但是,它也可能允许您使用更高级别的数据结构并在代码中实现更大的灵活性。
如今,它最常在 Java(和其他语言)的“自动装箱/自动拆箱”功能的上下文中讨论。这是一个以java 为中心的 autoboxing 解释。
在 .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
.
这被称为装箱,因为int
被object
. 当它的回退时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
然而,在处理装箱值时要小心另一件事。
Boxing
是将值类型转换为引用类型的过程。而Unboxing
将引用类型转换为值类型。
EX: int i = 123;
object o = i;// Boxing
int j = (int)o;// UnBoxing
值类型是:int
,char
和structures
, enumerations
。引用类型为
:Classes
、interfaces
、arrays
和strings
objects
.NET FCL 通用集合:
List<T>
Dictionary<TKey, UValue>
SortedDictionary<TKey, UValue>
Stack<T>
Queue<T>
LinkedList<T>
都是为了克服以前的集合实现中的装箱和拆箱的性能问题而设计的。
有关更多信息,请参阅第 16 章,通过 C# 进行 CLR(第 2 版)。
装箱和拆箱有助于将值类型视为对象。装箱意味着将值转换为对象引用类型的实例。例如,Int
是一个类,int
是一个数据类型。转换int
成是装箱Int
的一个例子,而转换Int
成int
是拆箱。这个概念有助于垃圾收集,另一方面,拆箱将对象类型转换为值类型。
int i=123;
object o=(object)i; //Boxing
o=123;
i=(int)o; //Unboxing.
盒子的语言无关含义只是“一个对象包含一些其他值”。
从字面上看,装箱是将一些值放入框中的操作。更具体地说,它是创建一个包含该值的新框的操作。装箱后,可以通过取消装箱从盒子对象中访问装箱后的值。
请注意,许多编程语言中的对象(不是特定于 OOP 的)是关于identities的,但值不是。两个对象是相同的iff。它们具有在程序语义中无法区分的身份。值也可以相同(通常在某些相等运算符下),但我们不会将它们区分为“一个”或“两个”唯一值。
提供盒子主要是为了将副作用(通常是突变)与对象的状态区分开来,否则用户可能看不到这些状态。
默认情况下,一种语言可能会限制访问对象的允许方式并隐藏对象的身份。例如,典型的 Lisp 方言在对象和值之间没有明确的区别。因此,实现可以自由地共享对象的底层存储,直到对象发生一些变异操作(因此必须在操作后将对象从共享实例中“分离”才能使效果可见,即变异存储在对象中的值可能与具有旧值的其他对象不同)。这种技术有时被称为对象实习。
如果对象是共享的而不需要频繁的变异,则实习使程序在运行时的内存效率更高,代价是:
std::basic_string
,即使以破坏至少一个主流实现 (libstdc++) 上的 ABI 为代价。可变单元,即盒子,是完善的设施,可以解决上面列出的第 1 和第 2 个问题。此外,可以有不可变的盒子来实现函数式语言中的赋值。实际例子见SRFI-111。
使用可变单元格作为按值调用策略的函数参数实现了在调用者和被调用者之间共享突变的可见效果。从这个意义上说,盒子包含的对象实际上是“被共享调用的”。
有时,这些框被称为引用(这在技术上是错误的),因此共享语义被命名为“引用语义”。这是不正确的,因为并非所有引用都可以传播可见的副作用(例如不可变引用)。引用更多地是关于通过间接公开访问,而框是努力公开访问的最小细节,例如是否间接(这是不感兴趣的,最好通过实现避免)。
此外,“价值语义”在这里是无关紧要的。值不反对引用,也不反对框。上面所有的讨论都是基于价值调用策略。对于其他人(如按名称调用或按需要调用),不需要框来以这种方式共享对象内容。
Java 可能是第一个使这些特性在业界流行的编程语言。不幸的是,这个话题似乎有很多不好的后果:
有关实现的更多提示(以及对此答案的评论):
double
对象无需拆箱即可访问其值,而某些其他类型的值可以装箱到宿主double
对象中,并且没有引用double
或装箱后的值。使用朴素指针方法,宿主对象指针的值就像PyObject*
是一个对象引用,它持有一个盒子,盒子的装箱值存储在动态分配的空间中。像其他任何事情一样,如果不小心使用,自动装箱可能会出现问题。经典的是最终得到一个 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;
}
}