CLR via C# 中关于装箱拆箱的摘录
装箱:
为了将一个值类型转换成一个引用类型,要使用一个名为装箱(boxing)的机制。下面总结了对值类型的一个实例进行装箱操作时在内部发生的事情。
1。在托管堆中分配好内存。分配的内存量是值类型的各个字段需要的内存量加上托管堆的所有对象都有的两个额外成员(类型对象指针和同步块索引)需要的内存量。
2。值类型的字段复制到新分配的堆内存。
3。返回对象的地址。现在,这个地址是对一个对象的引用,值类型现在是一个引用类型。
C#编译器会自动生成对一个值类型的实例进行装箱所需的IL代码,但你仍需理解内部发生的事情,否则很容易忽视代码长度问题和性能问题。
注意:
应该注意的是,FCL现在包含一组新的泛型集合类,它使非泛型的集合类成为“昨日黄花”。例如,应该使用System.Collection.Generic.List<T>类,而不要使用System.Collection.ArrayList类。泛型集合类在非泛型集合类的基础上进行了大量的增强。例如,API得到了简化和增强,集合类的性能也得到了显著提升。然而,最大的一个增强就是泛型集合类允许开发人员在操作值类型的集合时不需要对集合中的项进行装箱/拆箱处理。单单这一项设计,就是性能提升了不少。托管堆中需要创建的对象减少了,进而减少了应用程序需要执行的垃圾回收的次数。除此之外,开发人员还获得了编译时的类型安全性,源代码也因为强制类型转换的次数减少而变得更清晰。
拆箱:
假定需要使用以下代码获取ArrayList的第一个元素:
Point p=(Point)a[0];
现在是要获得ArrayList的元素0中的引用(或指针),并试图将其放到一个Point值类型的实例p中。为了做到这一点,包含在已装箱Point对象中的所有字段都必须复制到值类型变量p中,后者在线程栈上。CLR分两步完成这个复制操作。第一步是获取已装箱的Point对象中的各个Point字段的地址。这个过程称为拆箱(unboxing)。第二步是将这些字段包含的值从堆中复制到基于栈的值类型实例中。
拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低得多。拆箱其实就是获取一个指针的过程,改指针指向包含在一个对象中的原始值类型(数据字段)。事实上,指针指向的是已装箱实例中的未装箱部分。所以,和装箱不同,拆箱不要求在内存中复制任何字节。字段这个重要的区别之后,还应该知道的一个重点在于,往往会紧接着拆箱操作发生一次字段的复制操作。
装箱、拆箱例子:
Point p; p.x=p.y=1; Object o=p; //对p进行装箱;o引用已装箱的实例 //将Point的x字段变为2 p=(Point)o; //对o进行拆箱,并将字段从已装箱的实例复制到栈变量中 p.x=2; //更改栈变量状态 o=p; //对p进行装箱;o引用新的已装箱实例
两句话三次装箱操作
Int32 v=5; //创建一个未装箱的值类型变量 Object o=v; //o引用一个已装箱的、包含值5的Int32 v=123; //将未装箱的值改成123 Console.WriteLine(v + ", "+(Int32) o); //显示“123, 5”
应该指出的是,如果像下面这样写对WriteLine的调用,生成的IL代码将具有更高的执行效率:
Console.WriteLine(v +", "+o); //显示“123, 5”
甚至可以这样调用WriteLine,进一步提升上述代码的性能:
Console.WriteLine(V.ToString()+", “+o); //显示“123, 5”
现在,会为未装箱的值类型实例v调用ToString方法,它返回一个String。String对象已经是引用类型,所以能直接传给Concat方法,不需要任何装箱操作。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 #region Point 6 Point p1 = new Point(10, 10); 7 Point p2 = new Point(20, 20); 8 9 /*调用ToString()方法 10 * 在对ToString的调用中,p1不必装箱。从表面看,p1似乎必须装箱,因为ToString是从基类System.ValueType继承的一个虚方法。 11 * 通常,为了调用一个虚方法,CLR需要判断对象的类型,以定位类型的方法表。由于p1是一个未装箱的值类型,所以不存在“类型对象指针”。 12 * 然而,JIT编译器发现Point重写了ToString方法,所以会生成代码来直接(非虚)调用ToString方法,而不必进行任何装箱操作。 13 * 编译器知道这里不存在多态性的问题,因为Point是一个值类型,没有类型能从它派生,不可能存在该虚方法的其他实现。但是, 14 * 假如Point的ToString方法在内部调用base.ToString(),那么调用System.ValueType的ToString方法时,值类型的实例会被装箱。 15 */ 16 Console.WriteLine(p1.ToString()); 17 /* 调用非虚方法GetType 18 * 调用非虚方法GetType时,p1必须进行装箱。Point的GetType方法是从System.Object继承的。 19 * 所以,为了调用GetType,CLR必须使用指向一个类型对象的指针,而这个指针只能通过对p1进行装箱来获得。 20 */ 21 Console.WriteLine(p1.GetType()); 22 /* 调用CompareTo(第一次) 23 * 在CompareTo的第一次调用中,p1不必装箱,因为Point实现了CompareTo方法,编译器能直接调用它。注意, 24 * 我们像CompareTo传递了一个Point变量(p2),所以编译器会调用获取一个Point参数的CompareTo重载版本。 25 * 这意味着p2以传值方式传给CompareTo,无需装箱。 26 */ 27 Console.WriteLine(p1.CompareTo(p2)); 28 /*转型为IComparable 29 * 将p1转型为接口类型的变量c时,p1必须装箱,因为接口被定义为引用类型。p1在装箱之后,指向已装箱对象的指针 30 * 会存储到变量c中。后面对GetType的调用证明c确实引用了堆上的一个已装箱的Point。 31 */ 32 IComparable c = p1; 33 Console.WriteLine(c.GetType()); 34 /*调用CompareTo(第二次) 35 * 第二次调用CompareTo时,p1不需要装箱,因为Point实现了CompareTo方法,编译器能直接调用它。注意,此时向 36 * CompareTo传递的是一个IComparable类型的变量c,所以编译器会调用获取一个Object参数的CompareTo重载版本。 37 * 这意味着传递的实参必须是一个指针,它必须引用堆上的一个对象。幸好,c确实引用一个已装箱的Point,所以c中的内 38 * 存地址能直接传递给CompareTo,无需额外进行装箱。 39 */ 40 Console.WriteLine(p1.CompareTo(c)); 41 /*调用CompareTo(第三次) 42 * 第三次调用CompareTo时,c引用的已经是堆上的的一个已装箱Point对象。由于c是IComparable接口类型,所以只能 43 * 调用接口的需要获取一个Object参数的CompareTo方法。这意味着传递的实参必须是引用了堆上的一个对象的指针。 44 * 所以,p2会被装箱,指向这个已装箱对象的指针将传给CompareTo。 45 */ 46 Console.WriteLine(c.CompareTo(p2)); 47 /*转型为Point 48 * 将c转型为一个Point时,c引用的对上的对象会被拆箱,其字段会从堆复制到p2中。p2是栈上的一个Point类型的实例。 49 */ 50 p2 = (Point)c; 51 Console.WriteLine(p2.ToString()); 52 #endregion 53 Console.ReadKey(); 54 } 55 } 56 internal struct Point : IComparable 57 { 58 private int m_x,m_y; 59 //构造器负责初始化字段 60 public Point(Int32 x, Int32 y) 61 { 62 m_x = x; 63 m_y = y; 64 } 65 66 //重写从System.ValueType继承的ToString方法。 67 public override String ToString() 68 { 69 return String.Format("( {0} , {1} )", m_x, m_y); 70 } 71 72 public Int32 CompareTo(Point other) 73 { 74 return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) - Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y)); 75 } 76 77 public Int32 CompareTo(Object o) 78 { 79 if(GetType()!=o.GetType()) 80 { 81 throw new ArgumentException("o is not a Point"); 82 } 83 return CompareTo((Point)o); 84 } 85 }
前面说过,未装箱值类型是比引用类型更”轻型“的类型。这要归结于以下两个原因。
- 它们不在托管堆上分配。
- 它们没有堆上的每个对象都有的额外成员,也就是一个”类型对象指针“和一个”同步块索引“。
由于未装箱的值类型没有同步块索引,所以不能使用System.Threading.Monitor类型的各种方法(或者使用C#的lock语句)让多个线程同步对这个实例的访问。
注:上述文字皆摘自CLR via C#