刚开始学C#的时候就听过装箱和拆箱这两个名词,但是一直没有去了解这两个词是什么个意思,最近闲下来突然想到这个问题,就学了一发来blog上装装逼。
值类型和引用类型
在C#中,任何类型都直接或间接继承于class object
通常我们将变量的类型分为值类型和引用类型,它们在 [内存位置 传值/赋值方式] 等方面有着较大区别
– 内存位置
值类型的变量所处的内存位置在当前线程栈/函数栈中
引用类型的变量,实际内容处在托管堆中,在new时在托管堆中申请所需空间并执行一系列初始化动作;而我们可以将在代码中声明的某一变量视作指向该位置的指针,它将在当前线程栈/函数栈中占据一定空间
– 传值/赋值方式
简单理解,传值和赋值从某种角度上可以说是类似的,在调用函数时的传值可以视作发生了从实参到形参的赋值(虽然从汇编的角度看来两者是不同的)
值类型的传值/赋值方式是对对应内存的直接复制
两个int之间的赋值,是4个字节内存的复制,两个不同的变量实际是两个完全不同互不影响的变量,因此这里出现了经典的在C语言教学中几乎都会讲的问题:
void swap(int a, int b);
void swap(int *a, int *b);
两个引用类型的变量的赋值,可视作指针之间的复制,即在发生类似以下情况时:
RefType a = new RefType();
RefType b = a;
a和b两个变量所对应的在托管堆中的实际内存是同一块,但是在栈中的变量指针部分所处的内存位置是不同的
装箱和拆箱
装箱和拆箱问题出现在值类型变量中(引用类型变量总是处于已装箱状态)
值类型比引用类型更轻,它们的内容不在托管堆上分配,也不涉及到垃圾回收的问题
首先我们知道在C#中,将子类变量赋值给父类变量是允许的,而object是所有类型的基类,包括值类型
因此对于以下代码
Int32 intval = 0;
object obj = intval;
我们将值类型变量赋值给引用类型变量,值类型与引用类型的上文提到过的各种区别使得这样的赋值看似非常矛盾
在上面的代码中,obj是一个引用类型的变量,但是赋值的对象是一个值类型变量,因此这里需要将在栈中的值类型变量转换成一个在堆中托管的对象
装箱提供了将值类型变量转换成引用类型变量的处理方式
装箱需要三个步骤
1. 在托管堆中申请内存(值类型大小+类型对象指针+同步块索引)
2. 将值类型的各字段复制到新分配的堆内存中
3. 返回该引用对象的地址
Int32 newval = 1;
Object obj = newval;
在上述代码中,我们将引用类型的变量转换为值类型
不管是不是新声明的变量,都需要一个拆箱的过程
在拆箱时,堆中的引用类型变量中的各个字段会被复制到栈中值类型对应的内存上
拆箱不是装箱的一个逆过程,拆箱的开销比装箱低得多
看一个例子
Int32 val = 0;
Object obj = val;
Console.WriteLine(val + ", " + (Int32) obj);
在第二行中,发生了一次装箱
在第三行中发生了两次装箱
对于 val + ", " + (Int32) obj
, 调用的是string的静态方法
public static string Concat(Object arg0, Object arg1, Object arg2);
发生以下传值
Object arg0 = val;
Object arg1 = ", ";
Object arg2 = (Int32) obj;
由此可以看出,在第一行发生一次装箱,在第三行发生一次拆箱和装箱。
装箱需要额外的开销,因此会降低处理效率
如果采用以下的代码,程序效率将会更高
Console.WriteLine(val + ", " + obj); // 只发生一次装箱