为什么我们需要在C#中装箱和拆箱?

为什么我们需要在C#中装箱和拆箱?

我知道什么拳击和拆箱,但我不能理解它的真正用途。 为什么,我应该在哪里使用它?

short s=25; object objshort=s; //Boxing short anothershort=(short)objshort; //Unboxing 

为什么

为了有一个统一的types系统,并允许值types从引用types代表其基础数据的方式有一个完全不同的基础数据表示forms(例如,一个int只是一个三十二比特的桶,完全不同于参考types)。

像这样想。 你有一个objecttypes的variableso 。 现在你有一个int ,你想把它放到oo是对某个地方的引用,而int则强调不是对某个地方的引用(毕竟,它只是一个数字)。 所以,你所做的是这样的:你创build一个新的object ,它可以存储int ,然后为该对象分配一个引用到o 。 我们称这个过程为“拳击”。

所以,如果你不关心有一个统一的types系统(即引用types和值types有非常不同的表示,而且你不想用一种常见的方式来“表示”这两个),那么你就不需要装箱了。 如果你不关心int代表它们的基础值(也就是说int也是引用types,只是存储一个对其基础值的引用),那么你不需要装箱。

我应该在哪里使用它。

例如,旧的集合typesArrayList只吃object 。 也就是说,它只存储对某个地方居住的东西的引用。 没有拳击,你不能把一个int放入这样的集合。 但是用拳击,你可以。

现在,在generics的时代,你并不需要这些东西,而且一般都可以快乐地走,而不用考虑这个问题。 但是有一些注意事项要注意:

这是对的:

 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); 

在进行下一句话之前,先考虑一下。

如果你说TrueFalse真好! 等等,什么? 这是因为引用types==使用引用相等,它检查引用是否相等,而不是基础值是否相等。 这是一个非常容易犯的错误。 也许更加微妙

 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; px = 2; Console.WriteLine(((Point)o).x); 

什么是输出? 这取决于! 如果Point是一个struct那么输出是1但是如果Point是一个class那么输出是2 ! 一个装箱转换使得一个值框的副本解释行为的差异。

在.NET框架中,有两种types – 值types和引用types。 在OO语言中这是相当常见的。

面向对象语言的一个重要特性是能够以types无关的方式处理实例。 这被称为多态性 。 既然我们想利用多态性,但是我们有两种不同的types,就必须有一些方法把它们结合在一起,这样我们就可以用同样的方式来处理其中的一种。

现在回到过去的时代(微软.NET 1.0),没有这种新奇的genericshullabaloo。 你不能写一个只有一个参数可以服务一个值types和一个引用types的方法。 这是多态的违反。 所以拳击被采用作为强制价值types的一种手段。

如果这是不可能的,框架将散布的方法和类别,其唯一目的是接受其他types的种类。 不仅如此,由于值types并不真正共享一个共同的types祖先,所以你必须为每个值types(比特,字节,int16,int32等等)有一个不同的方法重载。

拳击阻止了这种情况的发生。 这就是为什么英国人庆祝节礼日。

拳击是不是真正的东西,你使用 – 这是运行时使用的,所以你可以在必要时以相同的方式处理引用和值types。 例如,如果您使用ArrayList来保存整数列表,则将整数装箱以适应ArrayList中的对象types插槽。

现在使用generics集合,这几乎消失。 如果创build一个List<int> ,则不会执行装箱 – List<int>可以直接保存整数。

理解这一点的最好方法是查看C#构build的底层编程语言。

在像C这样的最底层的语言中,所有的variables都在一个地方:堆栈。 每次你声明一个variables,它都会在栈上。 它们只能是原始值,比如bool,一个字节,一个32位的int,一个32位的uint等。堆栈既简单又快速。 当variables被添加时,它们只是一个在另一个之上,所以你声明的第一个位置是0x00,下一个位于0x01,下一个位于RAM的0x02等等。另外,variables通常在编译时被预编址,时间,所以他们的地址甚至在你运行程序之前就已经知道了。

在下一个层次上,像C ++一样,引入了第二种称为Heap的内存结构。 你仍然大部分都住在堆栈中,但是可以在堆栈中添加一个名为Pointer的特殊int,它存储Object的第一个字节的内存地址,并且该Object存在于堆中。 堆是一个混乱,维护费用有点昂贵,因为不同于堆栈variables,它们不会随着程序的执行而线性地堆积起来。 他们可以没有特定的顺序来来去去,他们可以成长和缩小。

处理指针是困难的。 它们是内存泄漏,缓冲区溢出和沮丧的原因。 C#来拯救。

在更高层次上,C#不需要考虑指针 – .Net框架(用C ++编写)为您考虑这些指针,并将它们作为对象的引用呈现给您,为了提高性能,您可以存储更简单的值像bools,字节和整数值types。 在底层,实例化一个类的对象和东西放在昂贵的,内存pipe理的堆上,而值types在同一个堆栈中,你在低级别C超级快。

为了保持这两个根本不同的内存概念(和存储策略)之间的相互作用,从编码人员的angular度来看很简单,价值types可以随时装箱。 拳击会导致从堆栈复制值,放入一个对象,并放置在堆 – 更昂贵,但参考世界的stream体交互。 正如其他答案指出的,当你举例说:

 bool b = false; // Cheap, on Stack object o = b; // Legal, easy to code, but complex - Boxing! bool b2 = (bool)o; // Unboxing! 

拳击的优势的一个强有力的例证是检查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,因为bool已经被装箱并放在那里。

一般来说,你应该避免拳击,除非你需要它,例如传递一个int / bool /无论作为一个对象的参数。 在.Net中有一些基本的结构,仍然要求传递值types作为对象(所以需要拳击),但大多数情况下,你永远不需要Box。

一个需要拳击的历史C#结构的非详尽列表,你应该避免:

  • 事件系统原来有一个竞争条件天真的使用它,它不支持asynchronous。 添加在拳击问题,它应该可能被避免。 (您可以将其replace为使用generics的asynchronous事件系统。)

  • 旧的线程和计时器模型强制在其参数上使用Box,但已被async / await所取代,它们更清洁,更高效。

  • .Net 1.1集合完全依赖于拳击,因为他们来到generics之前。 这些仍在System.Collections踢。 在任何新的代码中,你应该使用来自System.Collections.Generic的集合,除了避免拳击也为你提供更强的types安全性 。

你应该避免声明你的值types为Bool而不是boolInt而不是int等,因为你告诉.Net框架当你可能没有使用这个Boxing的时候把Boxvariables放在Box里。 有一个例外是,当你必须处理上述历史问题,强制拳击,并且你想避免拳击的性能打击,当你知道它将会是盒装无论如何。

根据Mikael的build议如下:

做这个

 using System.Collections.Generic; var employeeCount = 5; var list = new List<int>(10); 

不是这个

 using System.Collections; Int employeeCount = 5; var list = new ArrayList(10); 

装箱和拆箱专门用于将值types的对象作为引用types; 将其实际值移至托pipe堆并通过引用访问其值。

没有装箱和拆箱,你永远不能通过引用传递值types; 这意味着你不能作为Object的实例传递值types。

最后一个地方,我不得不解除一些东西,当编写一些从数据库中检索一些数据的代码时(我没有使用LINQ to SQL ,只是简单的旧的ADO.NET ):

 int myIntValue = (int)reader["MyIntValue"]; 

基本上,如果你使用generics之前的旧API,你会遇到拳击。 除此之外,这并不常见。

当我们有一个需要对象作为参数的函数时,需要装箱,但是我们有不同的需要传递的值types,在这种情况下,我们需要首先将值types转换为对象数据types,然后再传递给函数。

我不认为这是真的,试试这个:

 class Program { static void Main(string[] args) { int x = 4; test(x); } static void test(object o) { Console.WriteLine(o.ToString()); } } 

这运行得很好,我没有使用拳击/拆箱。 (除非编译器在幕后做这件事?)

在.net中,Object的每个实例或从中派生的任何types都包含一个包含其types信息的数据结构。 .net中的“真实”值types不包含任何此类信息。 为了允许值types的数据被期望接收从对象派生的types的例程操纵,系统自动为每个值types定义具有相同成员和字段的对应类types。 拳击会创build此类types的新实例,从值types实例复制字段。 拆箱将类types的实例中的字段复制到值types的实例。 所有从值types创build的类types都是从具有讽刺意味的类ValueType(尽pipe它的名称实际上是引用types)派生而来的。

当一个方法仅仅把一个引用types作为一个参数(比如一个generics方法被约束成一个通过new约束的类)时,你将无法传递一个引用types给它,

任何以object作为参数的方法也是如此 – 这必须是一个引用types。

一般来说,你通常会想避免装箱你的价值types。

然而,这种情况很less发生,这是有用的。 例如,如果您需要定位1.1框架,则无法访问generics集合。 在.NET 1.1中使用集合的任何使用将需要将您的值types视为System.Object,从而导致装箱/拆箱。

在.NET 2.0+中仍然有这种情况。 任何时候,如果想要利用包括值types在内的所有types都可以直接作为对象处理的事实,则可能需要使用装箱/拆箱。 这有时候会很方便,因为它允许你保存集合中的任何types(通过在generics集合中使用对象而不是T),但是总的来说,最好避免这种情况,因为你正在失去types安全性。 然而,经常出现拳击的一种情况就是当你使用Reflection时 – reflection中的许多调用在使用值types时将需要装箱/取消装箱,因为事先不知道types。