在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
摘要:说起装箱和拆箱,很多人都知道,也很清楚。简单地说,装箱就是值类型转换成引用类型的过程;拆箱就是引用类型转换成值类型过程。这么理解是没错的,我在此之前看到过很多关于装箱与拆箱的技术文章,说法有很多,深浅也各异。最近又在对 CLR via这本书复习了该知识,下面带你再认识老朋友装箱与拆箱,看看这两个过程究竟发生了些什么事情。 一、装箱(boxing)内部发生的事情 1.先看这段代码: 1 struct MyValue 2 { 3 public int a, b; 4 } 5 6 public sealed class Program 7 { 8 public static void Main() 9 { 10 MyValue v; 11 v.a = v.b = 0; 12 Check(v); 13 //... 14 } 15 16 private static void Check(object obj) 17 { 18 //... 19 } 20 } 其中可以看出,Check方法需要获取一个object类型的参数。即需要获取对托管对上的一个对象引用的来作为参数。但传入的参数是值类型的v,CLR如何使它正常接受并工作呢?对的,需要进行获取v的引用。 为了将一个值类型转换成引用类型,需要使用装箱(boxing)机制,装箱过程中发生了以下事情: 图:装箱过程线程栈和托管堆分配图 1.在托管堆中分配好内存。分配的内存量是值类型各个字段需要的内存量以及托管堆中所有对象都有的两个额外成员(类型对象指针和同步块索引)所需要的内存量 2.值类型的字段复制到新分配的堆内存。在托管堆中创建全新的实例。 3.返回对象的内存地址。上图中线程栈中V中保留已经在托管堆中分配好内存块的地址。 上述代码中,编译器检测到向一个需要引用类型的方法传递一个值类型,所以编译器会自动生成对一个值类型的实例进行装箱所需的IL代码。在运行时,值类型实例v中的字段会复制到托管堆中新分配的对象中,然后返回这个对象地址给Check方法。新创建的这个对象一直存在于堆中,直到被GC。而值类型MyValue定义的v可以被重用,可以对v的字段重新赋值使用,因为Check方法需要的是在托管堆新构建的对象。 二、拆箱 在知道了装箱之后,接着看看拆箱。假定有以下代码: 1 public static void Main() 2 { 3 object o = CreateValue(); 4 MyValue v = (MyValue)o; 5 //... 6 } 7 8 private static object CreateValue() 9 { 10 //... 11 MyValue v; 12 v.a = v.b = 0; 13 return v; 14 } 现在是获取到实例v的引用(或指针),并试图将其放入一个MyValue的值类型的实例v中。为了能做到这一点,这个实例的所有字段都必须复制到值类型v中,后者在线程栈上。这个过程是分两个步骤完成的:获取托管堆中实例v中字段a和b的地址。这个过程就是拆箱(unboxing)。第二步就是将这些字段包含的值从堆中复制到基于栈的值类型实例v中。 可以看出,拆箱的代价比装修要低得多,拆箱其实就是一个获取指针的过程。所以,拆箱不需要在托管堆中分配内存,不需要复制任何字节。 三、再看例子 从上面可以看出,装箱和拆箱操作都会对应用程序的速度和内存消耗产生不利影响,所以在编码的时候应该尽量手动避免。 1 int v = 4; 2 object o = v; 3 v = 123; 4 Console.WriteLine(v + "," + (int)o);//打印"123,4"; 上面几行代码共发生了3次装箱操作,别意外,来分析一下: 首先,第2行有一次明显的装箱。 主要看第4行,WriteLine方法要求传入一个String对象,但是,当前传入的并不是一个String对象,一个未装箱的int值类型(v),一个String类型,以及一个已装箱int值类型。(o)。C#编译器会调用String的静态方法Conact: public static String Concat(object arg0, object arg1, object arg2); 这里需要的参数arg0,arg1,arg2分别就对应上面的v,",",o。而Concat方法的需要的参数全是object引用类型,我们仔细一看,其中,v是一个未装箱的值类型,所以必须进行装箱,装箱完毕将新创建的实例地址传给arg0;第二个参数string,直接传递;第三个o首先会被转型为int类型,也就是先进行了一次拆箱操作,然后再进行装箱,把新地址传递给Concat的arg2参数,最后由Concat方法会调用指定每个对象的ToString方法。 所以,上面的第4行代码如果是如下写法,和之前写法几乎完全一致,只是少了o之前的int强制转换,生成的IL代码具有更高的执行效率。 Console.WriteLine(v + "," + o); 甚至可以进一步提升代码性能: Console.WriteLine(v.ToString() + "," + o) 由此可见,正因为额外的装箱步骤会从托管堆中分配额外的对象,将来必定造成对其进行垃圾回收,如果不注意,就会影响应用程序性能。 四、提示 1.如果知道自己的代码会造成编译器反复对一个值类型进行装箱,改成用手动方式对值类型进行装箱。 2.未装箱值类型比引用类型更“轻”,归结于a).它们不会在托管堆上分配;b).它们没有堆上的每个对象都有的额外成员(“类型对象指针”和“同步块索引”)。由于未装箱的值类型没有同步块索引,所以lock关键字不能接收,让多个线程对这个实例的访问。 注:通过写这篇文章来对以往书本知识的复习,加深对基础知识的掌握。参考资料:CLR via C# |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论