当前位置: 首页 > 科技观察

C#基础:理解装箱与拆箱

时间:2023-03-21 00:07:33 科技观察

转载请联系精致码农公众号。前面我们谈到了.NET平台支持的两种主要数据类型:值类型和引用类型。值类型比引用类型更高效,因为它没有指针引用,不需要在托管堆中分配,也不需要被GC回收。但有时您可能偶尔需要将一种类型的变量表示为另一种类型的变量。为此,C#提供了装箱和拆箱机制。1理解装箱简单来说,装箱就是将一个值类型的数据存储在一个引用类型的变量中。假设你在一个方法中创建了一个int类型的局部变量,你想把这个值类型表示为一个引用类型,那么就意味着你对这个值进行了装箱,如下所示:staticvoidSimpleBox(){intmyInt=25;//装箱操作objectboxedInt=myInt;}准确的说,装箱过程就是给一个Object类型变量赋值类型的过程。当您装箱一个值时,CoreCLR会在堆上分配一个新对象并将值类型的值复制到对象实例。返回给您的是对托管堆中新分配对象的引用。2理解拆箱反过来,将一个Object引用类型变量的值转换回栈中对应的值类型的过程称为拆箱。从句法上讲,拆箱操作看起来像一个普通的转换操作。然而,它的语义是完全不同的。CoreCLR首先验证接收到的数据类型是否等同于装箱类型,如果是,它将值复制回基于堆栈的局部变量。比如下面的boxedInt底层类型确实是int,那么拆箱操作就完成了:staticvoidSimpleBoxUnbox(){intmyInt=25;//装箱操作objectboxedInt=myInt;//拆箱操作intunboxedInt=(int)boxedInt;请记住,您必须将其拆箱为适当的数据类型,而不是执行典型的类型转换。如果您尝试将一段数据拆箱为不正确的数据类型,则会抛出InvalidCastException。为了保险起见,如果不能保证Object类型背后的类型,最好使用try/catch逻辑来包装拆箱操作,虽然这样会有点麻烦。考虑下面的代码,它会抛出错误,因为你试图将一个装箱的int类型拆箱为long类型:staticvoidSimpleBoxUnbox(){intmyInt=25;//装箱操作objectboxedInt=myInt;//拆箱到错误的数据类型,会触发运行时异常包含装箱/拆箱操作的代码。如果您查看使用ildasm.exe编译的程序集,您将看到用于装箱和拆箱操作的装箱和拆箱指令:.methodassemblyhidebysigstaticvoid'<

$>g__SimpleBoxUnbox|0_0'()cilmanaged{.maxstack1.localsinit(int32V_0,objectV_1,int32V_2)IL_0000:nopIL_0001:ldc.i4.s25IL_0003:stloc.0IL_0004:ldloc.0IL_0005:box[System.Runtime]System.Int32IL_000a:stloc.1IL_000b:ldloc.1ILRun_000c:untimebox.any[]System.0Int112.2IL_0012:ret}//endofmethod'$'::'<
$>g__SimpleBoxUnbox|0_0'乍一看,装箱/拆箱似乎是一个无用的语言特性,学术性而非实用性。毕竟,您很少需要将局部值类型存储在局部对象变量中。然而,事实是装箱/拆箱过程非常有用,因为它允许您假设一切都可以被视为对象类型,而CoreCLR会自动为您处理与内存相关的细节。4实际应用下面来看看装箱/拆箱的实际应用。下面以C#的ArrayList类为例,用它来保存一批存放在栈中的整型数据。ArrayList类的相关方法成员如下:]{得到;set;}}请注意,上面ArrayList的方法都是对Object类型数据进行操作的。ArrayList是为操作对象(代表任何类型)而设计的,对象是分配在托管堆上的数据。请考虑以下代码:staticvoidWorkWithArrayList(){//当传递给对象的方法时,值类型会自动装箱ArrayListmyInts=newArrayList();myInts.Add(10);}虽然你直接传递数值数据放入Object参数中,但运行时会自动装箱分配在堆栈上的数据。如果要使用索引器从ArrayList中检索项目,则必须使用强制转换将堆分配的对象拆箱为堆栈分配的整数,因为ArrayList的索引器返回一个对象,而不是一个int。staticvoidWorkWithArrayList(){//当传递给需要对象参数的方法时,值类型自动装箱ArrayListmyInts=newArrayList();myInts.Add(10);//当对象转换回基于堆栈的数据时,会发生拆箱inti=(int)myInts[0];//因为WriteLine()需要的对象参数被重新装箱Console.WriteLine("Valueofyourint:{0}",i);}在调用ArrayList之前。Add(),栈上分配的int值是装箱的,所以可以传递给参数类型为Object的方法。当从ArrayList中取出Object类型的数据时,通过转换操作将其拆箱为int类型。最后传给Console.WriteLine()方法的时候又被装箱了,因为这个方法的参数是Object类型的。5小结从程序员的角度来看,装箱和拆箱是非常方便的,我们不需要手动在内存中复制和传递值类型和引用类型的数据。但是装箱和拆箱背后的栈/堆内存传输也带来了性能问题。下面总结了装箱和拆箱一个简单整数所需的步骤:在托管堆中分配一个新对象;堆栈中的数据值被转移到托管堆中的对象;拆箱时,存储在堆中对象上的值被传输回堆栈;堆上未使用的对象最终会被GC回收。虽然很多时候装箱和拆箱操作不会对性能产生明显的影响,但是如果像ArrayList这样的集合包含了上千条数据,而你的程序频繁地对这些数据进行操作,对性能的影响还是很大的。明显地。因此,我们在编程时应尽量避免装箱和拆箱操作。比如上面的ArrayList例子,如果集合元素类型是一致的,就应该使用泛型的集合类型,比如List、LinkedList等。