装箱与拆箱

半城伤御伤魂 提交于 2020-02-28 07:08:55

知识点 

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

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

要掌握装箱与拆箱,就必须了解CTS及它的特点。

NET重要技术和基础之一的CTS(Common Type System)。顾名思义,CTS就是为了实现在应用程序声明和使用这些类型时必须遵循的规则而存在的通用类型系统。.Net将整个系统的类型分成两大类 ——Value Type 和 Reference Type。。,多数的OO语言存在这个弱点,原因就是因为他们的原类型没有共同的基点,于是他们在本质上并不是真正的对象C++更依赖于对象,而非面向对象。.Net环境的CTS 给我们带来了方便。第一、CTS中的所有东西都是对象;第二、所有的对象都源自一个基类——System.Object类型。这就是所谓的单根层次结构(singly rooted hierarchy)关于System.Object的详细资料请参考微软的技术文档。CTS  Value Type的一个最大的特点是它们不能为null,Value Type的变量总有一个值。在传递Value Type的变量时,实际传递的是变量的值,而非底层对象的“引用”。CTS  Reference Type就好像是类型安全的指针,它可以为null。当值为null时,说明没有引用或类型指向某个对象。声明一个引用类型的变量时,被操作的是此变量的引用(地址),而不是数据。 

    使用这种多类型系统时如何有效的拓展和提高系统的性能?就是今天探讨的问题,西雅图人提出了Box and UnBox的想法。简言之,装箱就是将value type转换为reference type;反之,就是拆箱。

装箱过程:

第一步将一个值压入堆栈;

第二步将引用类型转换为值类型;

第三步间接将值压栈;第四步传值给dubUnBox。

代码如下:

using System; 
namespace Box 

/// 
/// BoxAndUnBox 
的摘要说明。 
/// 
public class BoxAndUnBox 

public BoxAndUnBox() 

// 
// TODO: 在此处添加构造函数逻辑 
// 

///////////////////////////////////////////////////////////////////////////////////// 
static void Main(string[] args) 

double box1 =11.222; /// 定义一个值形变量 
object objBox =box1; /// 将变量的值装箱到 一个引用型对象中 
Console.WriteLine("The Value is '{0}' and The Boxed is {1}",box1,objBox.ToString()); 



}

打开ildasm.exe

MSIL代码如下:

.method private hidebysig static void Main(string[] args) cil managed 

.entrypoint 
// 
代码大小 42 (0x2a) 
.maxstack 3 
.locals init ([0] float64 box1, 
              [1] object objBox) 
IL_0000: nop

IL_0001: ldc.r8 11.222 
IL_000a: stloc.0 //
第IL_0000至IL_000a是定义值型变量的
IL_000b: ldloc.0 
IL_000c: box [mscorlib]System.Double 
IL_0011: stloc.1     //
第IL_000b 至 IL_0011 行是描述object objBox =box1代码的
IL_0012: ldstr "The Value is '{0}' and The UnBoxed is {1}"
IL_0017: stloc.0
IL_0018: box [mscorlib]System.Double
IL_001d: stloc.1 
IL_001e: callvirt instance string [mscorlib]System.Object::ToString()

IL_0023: call void [mscorlib]System.Console::WriteLine(string, 
object, 
object)

IL_0028: nop 
IL_0029: ret 
} // end of method BoxAndUnBox::Main

当box1被装箱时所发生的过程:

(1)划分堆栈内存,在堆栈上分配的内存 = box1的大小 + objBox及其结构所占用的空间;(2) box1的值(11.222)被复制到新近分配的堆栈中;

(3)将分配给objBox的地址压栈,此时它指向一个object类型,即引用类型。

拆箱过程:

装箱的逆过程。值得注意以下几点:box time不需要显式的类型转换,在unbox时就必须进行类型转换。因为引用类型的对象可以被转换为任何类型。电脑和人脑一个差别的体现就在于此!哈哈!类型转换不容回避的将会受到来自CTS管理中心的监控——其标准自然是依据规则。

下面这段代码:

using System; 
namespace UnBox 

/// 
/// BoxAndUnBox 
的摘要说明。 
/// 
public class BoxAndUnBox 

public BoxAndUnBox() 

// 
// TODO: 在此处添加构造函数逻辑 
// 

///////////////////////////////////////////////////////////////////////////////////// 
static void Main(string[] args) 

double box2 = 11.222; 
object objBox = box2; 
double dubUnBox = (double)objBox; /// 将引用型对象拆箱 ,并返回值 
Console.WriteLine("The Value is '{0}' and The UnBoxed is {1}",box2,dubUnBox); 

///////////////////////////////////////////////////////////////////////////////////// 

}

本段代码多加了一行double dubUnBox = (double)objBox;

这段代码的含义:

第一步将一个值压入堆栈;

第二步将引用类型转换为值类型;

第三步间接将值压栈;

第四步传值给dubUnBox。

.method private hidebysig static void Main(string[] args) cil managed 

.entrypoint 
// 
代码大小 48 (0x30) 
.maxstack 3 
.locals init ([0] float64 box1, 
[1] object objBox, 
[2] float64 dubUnBox) 
IL_0000: ldc.r8 77.769999999999996 
IL_0009: stloc.0 
IL_000a: ldloc.0 
IL_000b: box [mscorlib]System.Double 
IL_0010: stloc.1 
IL_0011: ldloc.1 
IL_0012: unbox [mscorlib]System.Double 
IL_0017: ldind.r8 
IL_0018: stloc.2 
IL_0019: ldstr "The Value is '{0}' and The UnBoxed is {1}" 
IL_001e: ldloc.0 
IL_001f: box [mscorlib]System.Double 
IL_0024: ldloc.2 
IL_0025: box [mscorlib]System.Double 
IL_002a: call void [mscorlib]System.Console::WriteLine(string, 
object, 
object) 
IL_002f: ret 
} // end of method BoxAndUnBox::Main

//

第IL_0011 至 IL_0018 行是描述double dubUnBox = (double)objBox代码的。

描述一下objBox在拆箱时的情况:(1)环境须先判断堆栈上指向合法对象的地址,以及在对此对象向指定的类型进行转换时是否合法,如果不合法,就抛出异常;(2)当判断类型转换正确,就返回一个指向对象内的值的指针。

改进:

为了避免由于无谓的隐式装箱所造成的性能损失,在执行这些多类型重载方法之前,最好先对值进行装箱。

代码改进:

using System; 
namespace NewBU 

/// 
/// BoxAndUnBox 
的摘要说明。 
/// 
public class BoxAndUnBox 

public BoxAndUnBox() 

// 
// TODO: 在此处添加构造函数逻辑 
// 

/////////////////////////////////////////////////////////////////// 
static void Main(string[] args) 

double box1 = 11.222; 
object objBox = box1; 
double dubUnBox = (double)objBox; 
object objUnBox = dubUnBox; 
Console.WriteLine("The Value is '{0}' and The UnBoxed is {1}",objBox,objUnBox); 

/////////////////////////////////////////////////////////////////// 

}  

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!