Answers:
为什么
要拥有统一的类型系统并允许值类型对其基础数据的表示形式与引用类型表示其基础数据的方式(例如, int
只是一个32位的存储桶,与引用完全不同)类型)。
这样想吧。您有一个o
类型的变量object
。现在您有了一个,int
并且想要将其放入o
。o
是对某处某物的引用,而int
强调不是某处某物的引用(毕竟,它只是一个数字)。因此,您要做的是:创建一个object
可以存储的新int
对象,然后将对该对象的引用分配给o
。我们称此过程为“装箱”。
因此,如果您不关心拥有统一的类型系统(即,引用类型和值类型具有非常不同的表示形式,并且您不希望使用通用的方式“表示”这两者),则无需装箱。如果您不关心int
表示其基础值(即也int
可以是引用类型,而只是存储对其基础值的引用),则不需要装箱。
我应该在哪里使用它。
例如,旧的集合类型ArrayList
只吃object
s。也就是说,它仅存储对居住在某处的事物的引用。没有拳击,你不能放一个int
这样的收藏中。但是使用拳击,您可以。
现在,在泛型时代,您实际上并不需要它,并且通常可以在不考虑问题的情况下轻松进行。但是需要注意一些注意事项:
这是对的:
double e = 2.718281828459045;
int ee = (int)e;
这不是:
double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception
相反,您必须执行以下操作:
double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;
首先,我们必须明确地取消double
((double)o
)的装箱,然后将其转换为int
。
以下结果是什么:
double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);
在继续下一句话之前,请先考虑一下。
如果你说的True
和False
伟大!等一下 那是因为==
在引用类型上使用引用相等性,它检查引用是否相等,而不是检查基础值是否相等。这是容易犯的危险。也许更加微妙
double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);
还将打印False
!
最好说:
Console.WriteLine(o1.Equals(o2));
幸运的是,它将打印出来True
。
最后一个微妙之处:
[struct|class] Point {
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);
输出是什么?这取决于!如果Point
是,struct
则输出为;1
如果Point
是,class
则输出为2
!装箱转换将复制装箱的值,以解释行为上的差异。
boxing
and 对性能的影响unboxing
?
在.NET框架中,有两种类型的类型-值类型和引用类型。这在OO语言中相对常见。
面向对象语言的重要特征之一是能够以与类型无关的方式处理实例。这称为多态。由于我们想利用多态性,但是我们有两种不同的类型,因此必须有某种方法将它们组合在一起,以便我们可以用相同的方式处理一个或另一个。
现在,在过去(Microsoft.NET的1.0)中,没有这种新奇的泛型hullabaloo。您无法编写具有单个参数的方法,该方法可以为值类型和引用类型提供服务。这违反了多态性。因此,采用装箱作为将值类型强制为对象的方法。
如果无法做到这一点,则该框架将堆满方法和类,这些方法和类的唯一目的是接受其他类型的类型。不仅如此,而且由于值类型并不真正共享一个共同的类型祖先,因此对于每种值类型(位,字节,int16,int32等),您都必须具有不同的方法重载。
拳击阻止了这种情况的发生。 这就是为什么英国人庆祝节礼日。
List<string>.Enumerator
会IEnumerator<string>
产生一个对象,该对象的行为基本上类似于类类型,但Equals
方法已损坏。强制转换List<string>.Enumerator
为更好的方法IEnumerator<string>
是调用自定义转换运算符,但是隐含转换的存在阻止了该转换。
理解这一点的最好方法是查看C#构建于其上的较低级编程语言。
在最低级的语言(如C)中,所有变量都放在一个位置:堆栈。每次声明变量时,它都会进入堆栈。它们只能是原始值,例如bool,字节,32位int,32位uint等。堆栈既简单又快速。随着变量的添加,它们只是一个接一个地运行,因此,您声明的第一个变量位于内存中,例如0x00,下一个位于0x01,下一个位于0x02,等等。此外,变量通常在compile-时间,因此甚至在运行程序之前就知道其地址。
与C ++一样,在下一个升级中,引入了第二种称为“堆”的内存结构。您仍然主要生活在堆栈中,但是可以将称为指针的特殊整数添加到堆栈中,以存储对象的第一个字节的内存地址,并且该对象位于堆中。堆有点混乱,并且维护起来有点昂贵,因为与堆栈变量不同,它们在程序执行时不会线性地递增然后递减。它们可以没有特定的顺序来来去去,并且可以成长和收缩。
处理指针很难。它们是导致内存泄漏,缓冲区溢出和失败的原因。用C#进行救援。
在更高级别的C#中,您无需考虑指针-.Net框架(用C ++编写)会为您考虑这些指针,并将其作为对对象的引用呈现给您,并且为了提高性能,您可以存储更简单的值如布尔值,字节和整数作为值类型。在幕后,实例化Class的Objects和东西放在昂贵的内存管理堆上,而Value Types和低级C放在同一堆栈中-超快。
为了使这两个根本不同的内存概念(和存储策略)之间的交互保持简单,从编码人员的角度出发,可以随时将“值类型”装箱。装箱会导致从堆栈中复制值,将其放入对象中,然后放置在堆中 -代价更高,但与参考世界的交互作用较不稳定。正如其他答案指出的那样,当您说:
bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!
关于Boxing优势的一个有力说明是对null的检查:
if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false
从技术上讲,我们的对象o是堆栈中的地址,该地址指向我们的bool b的副本,该副本已复制到堆中。我们可以检查o是否为null,因为布尔值已装箱并放在其中。
通常,除非需要,否则应避免使用Boxing,例如将int / bool / whatever作为对象传递给参数。.Net中有一些基本结构仍然需要将值类型作为对象传递(因此需要装箱),但是在大多数情况下,您不需要装箱。
需要装箱的历史C#结构的非详尽列表,应避免:
事实证明,事件系统在其天真的使用中具有“竞争条件”,并且不支持异步。添加拳击问题,应该避免。(例如,您可以将其替换为使用泛型的异步事件系统。)
旧的Threading和Timer模型在其参数上强制使用Box,但已被async / await替代,后者更加简洁高效。
.Net 1.1集合完全依赖Boxing,因为它们先于Generics。这些仍然在System.Collections中启动。在任何新代码中,您都应该使用System.Collections.Generic中的Collections ,除了避免Boxing之外,它还为您提供了更强的类型安全性。
您应该避免将值类型声明为对象或将其作为对象传递,除非必须处理上述导致装箱的历史问题,并且要避免在以后知道装箱的情况下装箱对性能的影响。
Per Mikael的建议如下:
using System.Collections.Generic;
var employeeCount = 5;
var list = new List<int>(10);
using System.Collections;
Int32 employeeCount = 5;
var list = new ArrayList(10);
该答案最初建议Int32,Bool等引起装箱,而实际上它们是值类型的简单别名。也就是说,.Net具有Bool,Int32,String和C#之类的类型,它们将别名化为bool,int,string,而没有任何功能上的区别。
装箱和拆箱专门用于将值类型的对象视为引用类型。将其实际值移动到托管堆并通过引用访问其值。
如果没有装箱和拆箱,则永远无法通过引用传递值类型。这意味着您不能将值类型作为Object的实例传递。
我最后要拆箱的地方是在编写一些代码以从数据库中检索某些数据时(我没有使用LINQ to SQL,而只是普通的ADO.NET):
int myIntValue = (int)reader["MyIntValue"];
基本上,如果您使用的是泛型之前的旧版API,则会遇到装箱问题。除此之外,它并不常见。
当我们有一个需要将对象作为参数的函数但需要传递不同的值类型时,则需要装箱。在这种情况下,我们需要先将值类型转换为对象数据类型,然后再将其传递给函数。
我认为这不是真的,请尝试以下方法:
class Program
{
static void Main(string[] args)
{
int x = 4;
test(x);
}
static void test(object o)
{
Console.WriteLine(o.ToString());
}
}
一切正常,我没有使用装箱/拆箱。(除非编译器在后台执行此操作?)
通常,您通常希望避免将值类型装箱。
但是,在少数情况下有用。例如,如果您需要针对1.1框架,则将无权访问通用集合。.NET 1.1中对集合的任何使用都需要将您的值类型视为System.Object,这会导致装箱/拆箱。
仍然有一些情况在.NET 2.0+中有用。每当您想利用所有类型(包括值类型)都可以直接作为对象的事实时,您可能需要使用装箱/拆箱。有时这会很方便,因为它允许您保存集合中的任何类型(通过在通用集合中使用object而不是T),但是通常最好避免这种情况,因为这样会丢失类型安全性。但是,经常发生装箱的一种情况是在使用Reflection时-使用值类型时,反射中的许多调用都需要装箱/拆箱,因为该类型是事先未知的。
装箱是将值转换为引用类型,并且数据在堆上的对象中处于某个偏移位置。
至于拳击实际上是做什么的。这里有些例子
单声道C ++
void* mono_object_unbox (MonoObject *obj)
{
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
}
#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
t result; \
MONO_ENTER_GC_UNSAFE; \
result = expr; \
MONO_EXIT_GC_UNSAFE; \
return result;
static inline gpointer
mono_object_get_data (MonoObject *o)
{
return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}
#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)
typedef struct {
MonoVTable *vtable;
MonoThreadsSync *synchronisation;
} MonoObject;
在Mono中取消装箱是一种在对象中偏移2个gpointer(例如16个字节)的指针的过程。A gpointer
是一个void*
。在查看的定义时,这很有意义,MonoObject
因为它显然只是数据的标头。
C ++
要在C ++中装箱值,您可以执行以下操作:
#include <iostream>
#define Object void*
template<class T> Object box(T j){
return new T(j);
}
template<class T> T unbox(Object j){
T temp = *(T*)j;
delete j;
return temp;
}
int main() {
int j=2;
Object o = box(j);
int k = unbox<int>(o);
std::cout << k;
}