对于值类型的装箱和拆箱,《CLR Via c#》一书中用了大量页数来讲,足以表明其重要性。
CTS有两种类型:值类型和引用类型。两种类型的互相转换通过装箱和拆箱完成。
装箱:将值类型转换成引用类型
拆箱:将引用类型转换成值类型
值类型存在两种形式:未装箱和已装箱
装箱内部实现过程:
1、在托管堆中分配内存。分配的内存量时值类型各字段所需的内存量,加上托管堆所有对象都会有的两个额外成员(类型对象指针和同步块索引)所需的内存量。
2、值类型的字段复制到新分配的堆内存中。
3、返回对象地址(引用)。
拆箱内部实现过程:
1、获取已装箱值类型对象中的各个字段的地址 — 这个过程就是拆箱(获取地址)
2、将字段包含的值从堆中赋值到基于栈的值类型实例中。
根据上面的过程可以看出,拆箱的成本要比装箱低很多,毕竟不需要开辟新的内存,且在内存中不复制任何字节(只是获取地址),后续第二步是紧跟拆箱后的一个字段复制操作。
看一段代码
int v = 5;
object o = v;
v = 123;
Console.WriteLine(v + "," + (int)o); //输出123,5
以上代码发生了3次装箱:
第一次装箱:object o = v;
当进行WtiteLine的时候,是执行了string.Concat方法。
public static string Concat(object arg0,object arg1,object arg2)
可以看到参数的类型是object,所以,打印的第一个v是int类型,需要转成object,这是第二次装箱。
接下来很明显,o转成了int,当然要输出的话,接着还会转成object,这是第三次装箱。
上述代码可以写成这样,只有一次装箱操作:
int v = 5;
object o = v;
v = 123;
Console.WriteLine(v.tostring() + "," + o); //输出123,5
关键是v.tostring()为什么没有装箱呢?因为值类型重写了Object的ToString()方法,直接返回值类型的字符串格式。所以就没有转object这样的装箱操作。
如果还要问为什么转成字符串不是装箱,首先第一点要理解装箱是什么。其次v.tostring()仅仅是创建了一个字符串而已,跟string str = “5”;一样。
再看一段代码:
int v= 5;
object o = v;
v = 123;
Console.WriteLine(v); //输出123
v = (int)o;
Console.WriteLine(v); //输出5
以上代码发生了1次装箱。
因为Console.WriteLine()有重载方法
public static void WriteLine(Int32); //我们平时写的int其实是Int32的简写,这个在这里不展开了。
所以看是否装箱,要先看一下方法的定义。
特别注意:(下面看一段原书中的话)
如果值类型重写了基类中任何虚方法,那么CLR可非虚的调用该方法,因为值类型隐式密封,不可能有类型从他们派生,而且调用虚方法的值类型实例没有装箱。然而,如果重写的虚方法要调用方法在基类中的实现,那么在调用基类的实现时,值类型实例会装箱,以便能通过this指针将堆一个堆对象的引用传给基方法。
看段代码来理解:
public struct Point
{
int x;
}
public struct Point1
{
int x;
public override string ToString()
{
return base.ToString();
}
}
public struct Point2
{
int x;
public override string ToString()
{
return x.ToString();
}
}
public void Main()
{
Point1 p1 = new Point1();
Point2 p2 = new Point2();
p1.ToString(); //p1会装箱,因为调用的是ValueType类的ToString()
p2.ToString(); //p2不会装箱,因为调用的是Int32.ToString()
Point p3 = new Point();
p3.ToString(); //p3会装箱,因为调用的是ValueType类的ToString()
p3.GetType(); //p3会装箱,因为是调用的Object.GetType();
}
关于装箱最后注意一点:
如果直到自己的代码会造成编译器返回对一个值类型装箱,请改成用手动 方式对值类型进行装箱,这样代码会变得更小,更快。
(何为手动,就是自己写代码先给他装箱,不要让编译器来处理)
未装箱的值类型比已装箱的值类型更“轻”,因为:
1、不在托管堆中分配
2、没有堆上的每个对象都有的额外成员:类型对象指针和同步块索引。