知识点
- 值类型。
- 值类型是在栈中分配内存,在声明时初始化才能使用,不能为null。
- 值类型超出作用范围系统自动释放内存。
- 主要由两类组成:结构,枚举(enum),结构分为以下几类:
- 整型(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)
- 浮点型(Float、Double)
- decimal
- bool
- 用户定义的结构(struct)
- 引用类型。
- 引用类型在堆中分配内存,初始化时默认为null。
- 引用类型是通过垃圾回收机制进行回收。
- 包括类、接口、委托、数组以及内置引用类型object与string。
概念
由于C#中所有的数据类型都是由基类System.Object继承而来的,所以值类型和引用类型的值可以通过显式(或隐式)操作相互转换,而这转换过程也就是装箱(boxing)和拆箱(unboxing)过程。
- 装箱 是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。对值类型装箱会在堆中分配一个对象实例,并将该值复制到新的对象中。
- 拆箱(取消装箱)是从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。取消装箱操作包括:
示例
首先写个简单的控制台程序:
// 装箱与拆箱
using System;
class App
{
static void Main()
{
int i = 32 ;
object o = i; // 隐式装箱
Console.WriteLine( " o = {0} " , o);
Console.Read();
}
}
其中object o = i这里我们进行了装箱操作,然后我们用MSIL 反汇编程序查看下生成的.exe程序的内部机理。
2 {
3 .entrypoint
4 // 代码大小 30 (0x1e)
5 .maxstack 2
6 .locals init ([ 0 ] int32 i,
7 [ 1 ] object o)
8 IL_0000: nop
9 IL_0001: ldc.i4.s 32
10 IL_0003: stloc. 0
11 IL_0004: ldloc. 0
12 IL_0005: box [mscorlib]System.Int32
13 IL_000a: stloc. 1
14 IL_000b: ldstr " o = {0} "
15 IL_0010: ldloc. 1
16 IL_0011: call void [mscorlib]System.Console::WriteLine( string ,
17 object )
18 IL_0016: nop
19 IL_0017: call int32 [mscorlib]System.Console::Read()
20 IL_001c: pop
21 IL_001d: ret
22 } // end of method App::Main
其中第12行是我们的装箱操作。(关于IL中出现的操作符代表的操作请查阅MSDN Library中的.NET开发/.NET Framework SDK/类库参考/System.Reflection.Emit/OpCodes 类/OpCodes 字段)
然后我们取消装箱操作:
{
int i = 32 ;
Console.WriteLine( " i = {0} " , i);
Console.Read();
}
再用MSIL工具查看生成的.exe,如下结果:
{
.entrypoint
// 代码大小 28 (0x1c)
.maxstack 2
.locals init ([ 0 ] int32 i)
IL_0000: nop
IL_0001: ldc.i4.s 32
IL_0003: stloc. 0
IL_0004: ldstr " i = {0} "
IL_0009: ldloc. 0
IL_000a: box [mscorlib]System.Int32
IL_000f: call void [mscorlib]System.Console::WriteLine( string ,
object )
IL_0014: nop
IL_0015: call int32 [mscorlib]System.Console::Read()
IL_001a: pop
IL_001b: ret
} // end of method App::Main
在IL_000a行,我们发现这里却也出现了一个box!不过这步是在call System.Console::WriteLine(string, object)时发生的。我们对比前面我们手动boxing的IL代码,发现在我们手动boxing后就没有这步box了。为什么呢?
当我们在调用一些方法的重载版本时,由于编译器找不到符合给定参数类型的重载方法,此时编译器便去寻找到的最接近的版本,然后使用找到的方法,而其参数却是我们传入的值类型的基类如System.Object或者其实现的接口类型,接着编译器为了求得与这个方法的原型一致,就必须对该值类型进行装箱操作(转换成引用类型)。
照这个说法当我们不手动boxing时,在调用了Console.WriteLine()方法输出一个Int32类型值时,系统就要自动进行boxing。也就是说如果我们要对该输出操作作5000次的循环,系统就要做5000次的boxing。这样对性能便会有一定的影响,而且要使循环次数是100,000,000次呢,或者跟多!
此时我们便要想如何消除这不应该的性能损失!正如第一个程序是展示的,我们可以在需要的地方先进行boxing,这个原理很简单,我们可以联想到类似的做法:
for ( int i = 0 ; i < arr.Length; i ++ )
{
//

}
// 我们更因该这样:
int L = arr.Length;
for ( int i = 0 ; i < L; i ++ )
{
//

}
这样,我们只要一次boxing,就可以避免让系统重复的做这个操作。
用途
像在调用Console.WriteLine()的过程中系统自动进行boxing一样,当我们在调用其它的一些方法的重载版本进行操所时,为了避免由于无谓的隐式装箱所造成的性能损失,在执行这些多类型重载方法之前,最好先对值进行装箱。一般是在处理大量数据需要对类型进行装箱操作。