引言
为什么有装箱和拆箱,两者起到什么作用?NET的所有类型都是由基类System.Object继承过来的,包括最常用的基础类型:int, byte, short,bool等等,就是说所有的事物都是对象。如果程序中所有的类型操作用的是引用类型时,往往导致效率低下,所以.Net通过将数据类型分为值类型和引用类型。
前面文章中讲过;
- 值类型
定义:值类型是在栈中分配内存,在声明时初始化后才能使用,不能为null。
a、整型:(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)
b、浮点型:(Float、Double)、decima、bool
c、用户定义的结构(struct)。
- 引用类型
定义:引用类型是在托管堆中分配内存空间用于存储数据、数据指针、以及Sync等,初始化默认为null。
包括类、接口、委托、数组以及内置引用类型object与string。
存储类型
什么是堆,什么是栈?
1、堆区(heap) 一般由程序员进行申请、释放,若程序员不释放,在程序退出时内存自动释放。
2、栈区(statck)- 由编译器自动分配释放,存放函数的参数值,局部变量的值
3、全局区(静态区)-全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
装箱和拆箱定义和过程
通俗上讲,装箱是讲值类型转化为引用类型,
拆箱是将引用类型转化为值类型 。
将值类型与引用类型链接起来为何需要装箱?(为何要将值类型转为引用类型?)
一种最普通的场景是,调用一个含类型为Object的参数的方法,该Object可支持任意为型,以便通用。当你需要将一个值类型(如Int32)传入时,需要装箱。
另一种用法是,一个非泛型的容器,同样是为了保证通用,而将元素类型定义为Object。于是,要将值类型数据加入容器时,需要装箱。
- 装箱操作(boxing):
1、首先从托管堆中为新生成的引用对象分配内存。
2、然后将值类型的数据拷贝到刚刚分配的内存中。
3、返回托管堆中新分配对象的地址。
可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。
- 拆箱操作(unboxing):
1、首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。
2、将引用对象中的值拷贝到位于线程堆栈上的值类型实例中。
经过这2步,可以认为是同boxing是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同boxing操作中一样影响性能。
- 代码实例:
c#代码
bject objValue = 4;
int value = (int)objValue;
上面的两行代码会执行一次装箱操作将整形数字常量4装箱成引用类型object变量objValue;然后又执行一次拆箱操作,将存储到堆上的引用变量objValue存储到局部整形值类型变量value中。
Il代码
locals init (
[0] object objValue,
[1] int32 'value'
) //上面IL声明两个局部变量object类型的objValue和int32类型的value变量
IL_0000: nop
IL_0001: ldc.i4.4 //将整型数字4压入栈
IL_0002: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
IL_0007: stloc.0 //弹出堆栈上的变量,将它存储到索引为0的局部变量中
IL_0008: ldloc.0//将索引为0的局部变量(即objValue变量)压入栈
IL_0009: unbox.any [mscorlib]System.Int32 //执行IL 拆箱指令unbox.any 将引用类型object转换成System.Int32类型
IL_000e: stloc.1 //将栈上的数据存储到索引为1的局部变量即value
上述代码中有几个box意味着有几次装箱操作,有几个unbox就是拆箱操作。
在拆箱中注意的问题:
int x = 0;
Int32 y = new Int32();
Object o ;
o = x; //隐式的装箱。
o = (Int32)y; //显示的装箱。
对于装箱而言,是不存在任何疑问点,既可以用显示(Explicit),也可以用隐式(Implicit)。
x = o; //Error;
x = (int)o 或者 x = (Int32)o; //Right;
拆箱必须是显示的,而不是隐式的。
Int32 x = 5;
Int64 y = 6;
object o;
o = x; or o = (Int32)x;
y = (Int64)o; //It's Error.
装箱的类型必须与拆箱的类型一致。而不是什么可隐式转换之类的。所以装箱的时候用的是Int32,拆箱的时候必须是Int32。
避免装箱的方法
通过泛型来避免。
- 非泛型集合
var array = new ArrayList();
array.Add(1);
array.Add(2);
foreach (int value in array)
{
Console.WriteLine(“value is {0}”,value);
}
在向ArrayList中添加int类型元素时会发生装箱,在使用foreach枚举ArrayList中的int类型元素时会发生拆箱操作,将object类型转换成int类型,在执行到Console.WriteLine时,还会执行两次的装箱操作;这一段代码执行了6次的装箱和拆箱操作;
- 泛型集合
var list = new List<int>();
list.Add(1);
list.Add(2);
foreach (int value in list)
{
Console.WriteLine("value is {0}", value);
}
代码和1中的代码的差别在于集合的类型使用了泛型的List,而非ArrayList;我们同样可以通过查看IL代码查看装箱拆箱的情况,上述代码只会在Console.WriteLine()方法时执行2次装箱操作,不需要拆箱操作。
通过重载函数来避免。
Struct A : ICloneable
{
public Int32 x;
public override String ToString() {
return String.Format(”{0}”,x);
}
public object Clone() {
return MemberwiseClone();
}
}
static void main()
{
A a;
a.x = 100;
Console.WriteLine(a.ToString());
Console.WriteLine(a.GetType());
A a2 = (A)a.Clone();
ICloneable c = a2;
Ojbect o = c.Clone();
}
5.0:a.ToString()。编译器发现A重写了ToString方法,会直接调用ToString的指令。因为A是值类型,编译器不会出现多态行为。因此,直接调用,不装箱。(注:ToString是A的基类System.ValueType的方法)
5.1:a.GetType(),GetType是继承于System.ValueType的方法,要调用它,需要一个方法表指针,于是a将被装箱,从而生成方法表指针,调用基类的System.ValueType。(补一句,所有的值类型都是继承于System.ValueType的)。
5.2:a.Clone(),因为A实现了Clone方法,所以无需装箱。
5.3:ICloneable转型:当a2为转为接口类型时,必须装箱,因为接口是一种引用类型。
5.4:c.Clone()。无需装箱,在托管堆中对上一步已装箱的对象进行调用。
有时我们可以提前进行装箱或者拆箱操作
比如:
//当我们如下时:
for (int i = 0; i < arrList.Length; i++)
{
//
}
//我们更因该这样:
int L = arrList.Length;
for (int i = 0; i < L; i++)
{
//
}
附:其实上面的基于一个根本的原理,因为未装箱的值类型没有方法表指针,所以,不能通过值类型来调用其上继承的虚方法。另外,接口类型是一个引用类型。对此,我的理解,该方法表指针类似C++的虚函数表指针,它是用来实现引用对象的多态机制的重要依据
凡事并不能绝对,假设你想改造的代码为第三方程序集,你无法更改,那你只能是装箱了。