装箱与拆箱

From: http://www.cnblogs.com/hunts/archive/2007/01/19/boxing_unboxing.html

C# 指南之装箱与拆箱

知识点

  1. 值类型。   
    1. 值类型是在栈中分配内存,在声明时初始化才能使用,不能为null。       
    2. 值类型超出作用范围系统自动释放内存。       
    3. 主要由两类组成:结构,枚举(enum),结构分为以下几类:       
      1. 整型(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)           
      2. 浮点型(Float、Double)           
      3. decimal           
      4. bool           
      5. 用户定义的结构(struct)
  2. 引用类型。   
    1. 引用类型在堆中分配内存,初始化时默认为null。       
    2. 引用类型是通过垃圾回收机制进行回收。       
    3. 包括类、接口、委托、数组以及内置引用类型object与string。

概念

由于C#中所有的数据类型都是由基类System.Object继承而来的,所以值类型和引用类型的值可以通过显式(或隐式)操作相互转换,而这转换过程也就是装箱(boxing)和拆箱(unboxing)过程。

  1. 装箱 是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。对值类型装箱会在堆中分配一个对象实例,并将该值复制到新的对象中。    
    • 装箱与拆箱_第1张图片  
  2. 拆箱(取消装箱)是从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。取消装箱操作包括:   
    1. 检查对象实例,确保它是给定值类型的一个装箱值。(拆箱后没有转成原类型,编译时不会出错,但运行会出错,所以一定要确保这一点。用GetType().ToString()判断时一定要使用类型全称,如:System.String 而不要用String。)

    2. 将该值从实例复制到值类型变量中。

    3. 装箱与拆箱_第2张图片

示例

首先写个简单的控制台程序:

// Tutorial_boxing_unboxing.cs
// 装箱与拆箱
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程序的内部机理。

1 .method private hidebysig static void   Main() cil managed
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 字段)

然后我们取消装箱操作:

    static void Main()
    {
       
int i = 32 ;

        Console.WriteLine(
" i = {0} " , i);

        Console.Read();
    }

再用MSIL工具查看生成的.exe,如下结果:

.method private hidebysig static void   Main() cil managed
{
  .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一样,当我们在调用其它的一些方法的重载版本进行操所时,为了避免由于无谓的隐式装箱所造成的性能损失,在执行这些多类型重载方法之前,最好先对值进行装箱。一般是在处理大量数据需要对类型进行装箱操作。

 

 

==============================================================

==============================================================

一、值类型(ValueType):直接存放真正的数据 ,值类型都有固定的长度,比如int占用4个字节,值类型的变量保存在堆栈上。作为值类型的变量,每个都有自己的数据,对一个变量的操作不影响其他变量。

 

二、引用类型(ReferenceType):存储对数据的内存地址的引用,位于受管制的堆上。堆用于存储可变长度的数据,比如字符串类型。作为引用类型的变量可以引用同一对象;因此对一个变量的操作会影响另一个变量所引用的同一对象。

 

三、栈(stack):(值类型数据存放在栈中)

1、栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;

2、栈是系统数据结构,对于进程/线程是唯一的;

3、栈空间分静态分配和动态分配两种。

4、方法结束时,栈中参数和局部变量所占内存自动释放。

 

四、堆(heap):(引用类型数据存放在堆中)

1、堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。

2、堆是函数库内部数据结构,不一定唯一。

3、堆空间的分配总是动态的。

4、方法结束时,对象所占内存并不自动释放。

 

五、值类型:

1、基元类型:整型、浮点型、小数型、布尔型,这些类型与其他语言的基本数据类型相似。

2、结构型:适用于创建小型对象。

3、枚举型:除char类型外的其它常数组成的列表类型。

 

六、引用类型:

1、可能的问题:当多个变量引用同样的内存块时,对任何一个引用变量的修改都会导致该对象的值的改变。

2、null:引用类型没有对任何实际地址进行引用。

3、字符串、类、数组、接口、委托。

 

七、值类型不能作为其他类型的基类:将类型声明为值类型的原则;该类型的行为类似于基元类型;该类型不需要继承自任何其他类型;该类型为会被任何其他类型继承;该类型的实例不会频繁地用于方法的参数传递;该类型的实例不会作为方法的结果频繁地返回;该类型的实例不会被频繁地用于集合中;

 

八、值类型相对于引用类型的差别:

1、值类型有装箱和未装箱两种形式,引用类型总是装箱形式。

2、值类型不能作为其他类型的基类。

3、引用类型初始化时为null,值类型初始化为0;

4、值类型变量赋值给另一个值类型变量时,会进行“字段对字段”的拷贝;引用类型变量值赋给另一个引用类型变量时,只会拷贝内存地址。

5、两个或多个引用类型变量可以指向托管堆中的同一个对象;值类型变量数据是独立的。

6、内存回收问题:值类型自动,引用类型GC。

 

九、装箱(boxing):

1、将值类型转换为引用类型,并返回引用的过程。

2、经过装箱的值类型的生存周期超过未装箱实例。

 

十、装箱的步骤:

1.在托管堆中为新生成的引用类型对象分配内存

(内存大小为值类型大小及2个附加成员)。

2. 将值类型实例的字段拷贝到托管堆上新分配对象的内存中。

3. 返回托管堆中新分配对象的地址—指向该对象的引用。

 

十一、拆箱(unboxing)

1、获取指向对象中包含的值类型部分的指针(该指针指向已装箱对象的未装箱部分)。

2、装箱和拆箱并不是严格意义上的互反操作:(装箱:分配内存、拷贝字段、返回指针。拆箱:获取指针(不拷贝这字段))。

3、拆箱之后的典型操作往往就是字段拷贝。

4、拆箱操作时,转型结果必须是它原来未装箱的类型。

 

十二、拆箱的步骤:

1、获取指向对象中包含的值类型部分(数据字段)的指针(该指针指向已装箱对象的未装箱部分)。

2、拆箱操作本身并不拷贝字段!!

你可能感兴趣的:(装箱与拆箱)