在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制。
public static void Main(){ Int32 x = 5; Object o = x; Int16 y = (Int16) o;//抛出异常 }
在对一个对象进行拆箱的时候,只能将其转型为原始未装箱时的值类型——Int32,下面是正确的写法:
public static void Main(){ Int32 x = 5; Object o = x; //对x进行装箱,o引用已装箱的对象 Int16 y = (Int16) (Int32) o; //先拆箱为正确的类型,在进行装箱 }
前面说过,在进行一次拆箱后,经常会紧接着一次字段的复制。以下演示了拆箱和复制操作:
public static void Main() { Point p = new Point(); //栈变量 p.x = p.y = 1; object o = p; //对p进行装箱,o引用已装箱的实例 p = (Point) o; //对o进行拆装,将字段从已装箱的实例复制到栈变量 }
在最后一行,C#编译器会生成一条IL指令对o进行拆箱,并生成另一条IL指令将这些字段从堆复制到基于栈的变量p中。
再看看一下代码:
public static void Main() { Point p = new Point(); // 栈变量 p.x = p.y = 1; object o = p; // 对p进行装箱,o引用已装箱的实例 // 将Point的x字段变成2 p = (Point) o; // 对o进行拆装,将字段从已装箱的实例复制到栈变量 p.x = 2; // 更改变量的状态 o = p; // 对p进行装箱,o引用已装箱的实例 }
最后三行代码唯一的目的就是将Point的x字段从1变成2.为此,首先要执行一次拆箱,在执行一次字段复制,在更改字段(在栈上),最后执行一次装箱(从而在托管堆上创建一个全新的已装箱实例)。希望你能体会到装箱和拆箱/复制操作对应用程序性能的影响。
在看个演示装箱和拆箱的例子:
private static void Main(string[] args) { Int32 v = 5; // 创建一个伪装箱的值类型变量 Object o = v; // o引用一个已装箱的、包含值5的Int32 v = 123; // 将未装箱的值修改成为123 Console.WriteLine(v + "," + (Int32)o); //显示"123,5" }
你可以看出上述代码进行了几次装箱操作?如果说是3次,你会不会意味呢?我们来看下生成的IL代码。
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint //代码大小 .maxstack 3 .locals init ( [0] int32 num, [1] object obj2) L_0000: nop // 将5加载到v中 L_0001: ldc.i4.5 L_0002: stloc.0 // 对v进行装箱,将引用指针存储到o中 L_0003: ldloc.0 L_0004: box int32 L_0009: stloc.1 // 将123加载到v中 L_000a: ldc.i4.s 0x7b L_000c: stloc.0 // 对v进行装箱,并将指针保留在栈上以进行Concat(连接)操作 L_000d: ldloc.0 L_000e: box int32 // 将字符串加载到栈上以执行Concat操作 L_0013: ldstr "," // 对o进行拆箱:获取一个指针,它指向栈上的Int32的字段 L_0018: ldloc.1 L_0019: unbox.any int32 // 对Int32进行装箱,并将指针保留在栈上以进行Concat(连接)操作 L_001e: box int32 // 调用Concat L_0023: call string [mscorlib]System.String::Concat(object, object, object) // 将从Concat放回的字符串传给WriteLine L_0028: call void [mscorlib]System.Console::WriteLine(string) L_002d: nop // 从Main返回 L_002e: ret }
提示:主要原因是在Console.WriteLine方法上。
Console.WriteLine方法要求获取一个String对象,为了创建一个String对象,C#编译器生成的代码来调用String对象的静态方法Concate。该方法有几个重载的版本,唯一区别就是参数数量,在本例中需要连接三个数据项来创建一个字符串,所以编译器会选择以下Concat方法来调用:
public static String Concat(Objetc arg0, Object arg1, Onject arg2);
所以,如果像下面写对WriteLine的调用,生成的IL代码将具有更高的执行效率:
Console.WriteLine(v + "," + o); //显示"123,5"
这只是移除了变量o之前的(Int32)强制转换。就避免了一次拆箱和一次装箱。
我们还可以这样调用WriteLine,进一步提升上述代码的性能:
Console.WriteLine(v.ToString() + "," + o); //显示"123,5"
现在,会为未装箱的值类型实例v调用ToString方法,它返回一个String。String类型已经是引用类型,所以能直接传给Concat方法,不需要任何装箱操作。
下面在演示一个装箱和拆箱操作:
private static void Main(string[] args) { Int32 v = 5; // 创建一个伪装箱的值类型变量 Object o = v; // o引用一个已装箱的、包含值5的Int32 v = 123; // 将未装箱的值修改成为123 Console.WriteLine(v) //显示"123" v = (Int32) o; //拆箱并将o复制到v Console.WriteLine(v); //显示"5" }
上述代码发生了多少次装箱呢?答案是一次。因为System.Console类定义了获取一个Int32作为参数的WriteLine方法的重载版本:
public static String Concat(Int32 value);
在WriteLine方法内部也许会发生装箱操作,但这已经不是我们能控制的。我们已经尽可能地从自己的代码中消除了装箱操作。
最后,如果知道自己写的代码会造成编译器反复对一个值类型进行装箱,请改用手动方式对值类型进行装箱。
对象相等性和同一性。System.Object类型提供了一个名为Equals的虚方法,它的作用是在两个对象包含相同的值得前提下返回true。如:
public class Object{ publick virtual Boolean Equals(Object obj) { //如果两个引用指向同一个对象,它们肯定包含相同的值 if ( this == obj ) return true; //假定对象不包含相同的值 return false; } }
对于Object的Equals方法的默认实现来说,它实现的实际是同一性,而非相等性。
public class Object{ public virtual Boolean Equals(Object obj) { //要比较的对象不能为null if (obj == null ) return false; //如果对象类型不同,则肯定不相等 if (this.GetType() != obj.GetType()) return false; //如果对象属于相同的类型,那么在它们所有字段都匹配的前提下返回true //由于System.Object没有定义任何字段,所以字段是匹配的 return true; } }
由于,一个类型能重写Object的Equals方法,所以不能再调用这个Equals方法来测试同一性。为了修正这一问题,Object提供了一个静态方法ReferenEquals,其原型如下:
public class Object{ public static Boolean ReferenceEquals(Object objA , Object objB) { retuen ( onjA == objB ); } }
如果想检查同一性,务必调用ReferenceEquals,而不应该使用C#的== 操作符,因为==操作符可能被重载。