十六:值类型的装箱和拆箱(二)
看以下代码:
public static void Main()
{
Int32 v = 5; //创建一个未装箱的值类型变量
Object o = v; //o引用一个已装箱的Int32,包含值5
v = 123; //将未装箱的值修改成123
Console.WriteLine(v + "," + (Int32)o);
}
以上代码发生了几次装箱,答案是3次。
首先在堆栈上创建一个Int32未装箱值类型的实例(v),并将其初始化为5,然后,创建一个Object类型的变量(o),并初始化它,让它指向v。但是,由于引用类型的变量必须指向堆中的对象,所以C#编译器会自动生成IL代码对v进行装箱,再将v的已装箱地址存储到o中。现在值123要放到未装箱的值类型实例v中,但这这个操作不会影响已装箱的Int32。
接着要调用WriteLine方法,该方法要求向其传递一个String对象,当前只有三个数据项:一个未装箱的Int32值类型实例(v)、一个String(它是一个引用类型),已及一个已装箱的Int32值类型的实例的引用(o),它要转换成一个未装箱的Int32,必须采用某种方式对这些数据项进行合并,以创建一个String。
C#编译器会生成代码来调用String对象的静态方法Concat,该方法有几个重载的版本,所有的版本执行的操作都一样,只是参数数量不一样,根据上面情况,选用的是Concat的以下版本:(注:在VS中把光标放到String上,单击右键,转到定义即可看到该类型的定义)
public static string Concat(object arg0, object arg1, object arg2);
为第一个参数arg0传递的是v,但是v是一个未装箱的值参数,而arg0是一个Object,所以必须对v进行装箱,并将v的地址传给arg0。为arg1参数传递的是字符串“,”,它是对一个String对象的引用,最后,arg2参数o会拆箱转型为一个Int32,从而获得包含在已装箱的Int32中的未装箱的Int32的地址,这个未装箱的Int32必须再次装箱,并将新的已装箱的实例的内存地址传给Concat的arg2参数。
Concat方法调用指定的每个对象的ToString方法,并连接每个对象的字符串表示。从Concat传回的String对象随即传给WriteLine方法,显示最终结果。 如果以上代码写成这样效率会更高些:
Console.WriteLine(v + "," + o);
移除了o前面的一个拆箱操作,之所以这样是因为o已经是一个Object引用类型,它的地址可以直接传给Concat方法,所以这样就少了一个拆箱和一个装箱的操作,提高了性能。
还可以这样写:
Console.WriteLine(v.ToString() + "," + o);
在未装箱的值类型实例v上调用ToString()方法,返回一个String,String对象是引用类型,能直接传给Concat方法,不需要任何装箱操作。
再看以下代码:
public static void Main()
{
Int32 v = 5; //创建一个未装箱的值类型变量
Object o = v; //o引用一个已装箱的Int32,包含值5
v = 123; //将未装箱的值修改成123
Console.WriteLine(v);//显示123
v = (Int32)o; //拆箱并将o复制到v
Console.WriteLine(v);//显示5
}
上面代码只发生了一次装箱,因为Console.WriteLine()方法有一个重载版本只接受一个Int32值作为参数:
public static void WriteLine(Int32 value);
假如知道自己写的代码会进行反复对一个值类型进行装箱,那么手动方式对值类型装箱会有更好的效果,如下:
public static void Main()
{
Int32 v = 5; //创建一个未装箱的值类型变量
//编译这一行是,v会被装箱三次
Console.WriteLine("{0},{1},{2}", v, v, v);
//下面v只被装箱一次
Object o = v;
Console.WriteLine("{0},{1},{2}", o, o, o);
}