在对这个问题展开讨论之前,我们不妨先来问这么几个问题,以系统的了解我们今天要探究的主题。

观者也许曾无数次的使用过诸如System.Console类或.NET类库中那些品种繁多的类。那么,我想问的是它们究竟源自何处?C#又是如何联系它们?有没有支持我们个性化扩展的机制或类型系统?又有哪些类型系统可供我们使用呢?如果我们这些PL们连这些问题都不知其然,更不知其所以然的话,C#之门恐怕会把我们拒之门外的。

那就让我们先停停手中的活儿,理理头绪,对作为.NET重要技术和基础之一的CTS(Common Type System)做一个饶有兴趣的研究。顾名思义,CTS就是为了实现在应用程序声明和使用这些类型时必须遵循的规则而存在的通用类型系统。在这要插一句,虽然也许大家都对此再熟悉不过了,但是我还是要强调,.Net将整个系统的类型分成两大类 —— 值类型 和 引用类型。到此,你也许会怒斥:说了这么半天,你似乎还没有切入正题呢!别慌!知道了.Net类型系统的的特点并不代表你真正理解了这个类型系统的原理和存在的意义。

大多数面向对象的语言都有两种类型:原类型(语言固有的类型,如整数、枚举)和类。虽然在实现模块化和实体化方面,面向对象技术体现了很强的能力,但是也存在一些问题,比如现在提到的这个系统类型问题,历史告诉我们两组类型造成了许多问题。首先就是兼容性问题,这个也是Microsoft使劲抨击的一点,多数的OO语言存在这个弱点,原因就是因为他们的原类型没有共同的基点,于是他们在本质上并不是真正的对象,它们并不是从一个通用基类里派生来的。怪不得,Anders Heijlsberg 笑称其为“魔术类型”。

正是由于这一缺陷,当我们希望指定一个可以接受本语言支持的任何类型的参数的Method时,同样的问题再次袭扰我们的大脑——不兼容。当然,对于C++的PL大拿,也许这个没有什么大不了的,他们会自豪的说,只要用重载的构造器为每一种原类型编写一个Wrapper Class 不就完了嘛!好吧,这样总算是能共存了,但是,接下来我们怎么从这个魔术中得到我们最关心的东东 —— 结果呢?于是,他们依然会自信的打开Boarland,熟练的编写一个重载过的函数来从刚才的那个 Wrapper Class 中获取结果。兄弟 or 姐妹们 ,在当时的历史条件下,你们的行为是创举,但是相对于现在,你将会为此付出代价 —— 效率低下。毕竟,C++更依赖于对象,而非面向对象。承认现实总比死要面子更理智一些!花这么大力气,总算把铺垫说完了,我想说的是:.Net环境的CTS 给我们带来了方便。第一、CTS中的所有东西都是对象;第二、所有的对象都源自一个基类——System.Object类型。这就是所谓的单根层次结构(singly rooted hierarchy)关于System.Object的详细资料请参考微软的技术文档。这里我们简略的谈谈上面提到过的两大类型:Value Type 和 Reference Type。

CTS值类型的一个最大的特点是它们不能为null,言外之意就是值类型的变量总有一个值。在C#中,它包括有原类型、结构、枚举器。这里需要强调一点:在传递值类型的变量时,我们实际传递的是变量的值,而非底层对象的引用,这一点和传递引用类型的变量的情况截然不同;CTS引用类型就好像是类型安全的指针,它可以为null。它包括 如类、接口、委托、数组等类型。对比前面值类型的特点,当我们分配一个引用类型时,系统会在后台的堆栈上分配一个值(内存分配与位置)并返回对这个值的引用;当值为null时,说明没有引用或类型指向某个对象。这就意味着,我们在声明一个引用类型的变量时,被操作的是此变量的引用(地址),而不是数据。

讨论到这个地方的时候,本篇的主角终于闪亮登场了——欲吐血或者呕吐的同志,请再忍耐一下。我想问一个问题先:在使用这种多类型系统时如何有效的拓展和提高系统的性能?也许就是在黑板上对这个问题的探讨,西雅图的那帮家伙们提出了Box(装箱) and UnBox(拆箱) 的想法。简单的说。装箱就是将值类型(value type)转换为引用类型(reference type)的过程;反之,就是拆箱。(其实这种思想早八辈子就产生了)。下面我们就进一步详细的讨论装箱和拆箱的过程。在讨论中,我们刚刚提到的问题的答案也就迎刃而解了。

首先,我们先来看看装箱过程,为此我们需要先做两个工作:1、编写例程; 2、打开ILDASM(MSIL代码察看工具)为此我们先来看看以下的代码:
 
using System;
 
namespace StructApp
{
/// 
/// BoxAndUnBox 的摘要说明。
/// 
public class BoxAndUnBox
{
public BoxAndUnBox()

//
// TODO: 在此处添加构造函数逻辑
//
}
///////////////////////////////////////////////////////////////////////////////////// 
static void Main(string[] args)
{
double dubBox = 77.77; /// 定义一个值形变量 
object objBox = dubBox; /// 将变量的值装箱到 一个引用型对象中 
Console.WriteLine("The Value is '{0}' and The Boxed is {1}",dubBox,objBox.ToString());
}
/////////////////////////////////////////////////////////////////////////////////////
}
}

代码中,本篇我们只需要关注Main()方法下加注释的两行代码,第一行我们创建了一个double类型的变量(dubBox)。显然按规则,CTS规定double是原类型,所以dubBox自然就是值类型的变量;第二行其实作了三个工作,这个将在下面的MSIL代码中看的一清二楚。第一步取出dubBox的值,第二步将值类型转换引用类型,第三步传值给objBox。

MSIL代码如下:
 
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 40 (0x28)
.maxstack 3
.locals init ([0] float64 dubBox,
[1] object objBox)
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: ldstr "The Value is '{0}' and The Boxed is {1}"
IL_0016: ldloc.0
IL_0017: box [mscorlib]System.Double
IL_001c: ldloc.1
IL_001d: callvirt instance string [mscorlib]System.Object::ToString()
IL_0022: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_0027: ret
} // end of method BoxAndUnBox::Main

在MSIL中,第IL_0000 至 IL_0010 行是描述前面两行代码的。参照C#的MSIL手册,观者不难理解这段底层代码的执行过程,在这我着重描述一下当dubBox被装箱时所发生的故事:(1)划分堆栈内存,在堆栈上分配的内存 = dubBox的大小 + objBox及其结构所占用的空间;(2)dubBox的值(77.7699999999996)被复制到新近分配的堆栈中;(3)将分配给objBox的地址压栈,此时它指向一个object类型,即引用类型。

拆箱作为装箱的逆过程,看上去好像很简单,其实里面多了很多值的思考的东西。首先,box的时候,我们不需要显式的类型转换,但是在unbox时就必须进行类型转换。这是因为引用类型的对象可以被转换为任何类型。(当然,这也是电脑和人脑一个差别的体现)类型转换不容回避的将会受到来自CTS管理中心的监控——其标准自然是依据规则。(其内容的容量足以专门设一章来讨论)好了,我们还是先来看看下面这段代码吧:

using System;

namespace StructApp
{
/// 
/// BoxAndUnBox 的摘要说明。
/// 
public class BoxAndUnBox
{
public BoxAndUnBox()

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

/////////////////////////////////////////////////////////////////////////////////////
static void Main(string[] args)
{
double dubBox = 77.77; 
object objBox = dubBox; 
double dubUnBox = (double)objBox; /// 将引用型对象拆箱 ,并返回值
Console.WriteLine("The Value is '{0}' and The UnBoxed is {1}",dubBox,dubUnBox);
}
/////////////////////////////////////////////////////////////////////////////////////
}
}

与前面装箱的代码相比,本段代码多加了一行double dubUnBox = (double)objBox;新加的这行代码作了四个工作,这个也将体现在MSIL代码中。第一步将一个值压入堆栈;第二步将引用类型转换为值类型;第三步间接将值压栈;第四步传值给dubUnBox。

MSIL代码如下:
 
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 48 (0x30)
.maxstack 3
.locals init ([0] float64 dubBox,
[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

在MSIL中,第IL_0011 至 IL_0018 行是描述新行代码的。参照C#的MSIL手册,观者不难理解这段底层代码的执行过程,在此我着重描述一下objBox在拆箱时的遭遇:(1)环境须先判断堆栈上指向合法对象的地址,以及在对此对象向指定的类型进行转换时是否合法,如果不合法,就抛出异常;(2)当判断类型转换正确,就返回一个指向对象内的值的指针。

看来,装箱和拆箱也不过如此,费了半天劲,刚把‘值’给装到‘箱’里去了,有费了更多的劲把它拆解了,郁闷啊!细心的观者,可能还能结合代码和MSIL看出,怎么在调用Console.WriteLine()的过程中又出现了两次box,是的,我本想偷懒逃过这节,但是既然已被发现,就应该大胆的面对,其实这就是传说中的“暗箱操作”啊! 因为Console.WriteLine方法有许多的重载版本,此处的版本是以两个String对象为参数,而具有object 类型的参数的重载是编译器找到的最接近的版本,所以,编译器为了求得与这个方法的原型一致,就必须对值类型的dubBox和dubUnBox分别进行装箱(转换成引用类型)。

所以,为了避免由于无谓的隐式装箱所造成的性能损失,在执行这些多类型重载方法之前,最好先对值进行装箱。现在我们把上述地代码改进为:
 
using System;

namespace StructApp
{
/// 
/// BoxAndUnBox 的摘要说明。
/// 
public class BoxAndUnBox
{
public BoxAndUnBox()

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

///////////////////////////////////////////////////////////////////
static void Main(string[] args)
{
double dubBox = 77.77; 
object objBox = dubBox; 
double dubUnBox = (double)objBox; 
object objUnBox = dubUnBox;
Console.WriteLine("The Value is '{0}' and The UnBoxed is {1}",objBox,objUnBox);
}
///////////////////////////////////////////////////////////////////
}
}

MSIL代码:
 
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 45 (0x2d)
.maxstack 3
.locals init ([0] float64 dubBox,
[1] object objBox,
[2] float64 dubUnBox,
[3] object objUnBox)
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: ldloc.2
IL_001a: box [mscorlib]System.Double
IL_001f: stloc.3
IL_0020: ldstr "The Value is '{0}' and The UnBoxed is {1}"
IL_0025: ldloc.1
IL_0026: ldloc.3
IL_0027: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002c: ret
} // end of method BoxAndUnBox::Main

我晕!这算嘛事儿呀!看完后是不是该吐血的吐血,该上吊的上吊呀!相信能坚持到看完最后一个 "!" 的同志一定是个好同志。

其实,我们也可以妄加揣测一下:引用型应当属于高级类型,而值型属于原始类型,箱只是一个概念、一个秩序、一套规则或准确说是一个逻辑。原始的东西作为基础,其复杂性和逻辑性不会很高,而高级的东西就不那么稳定了,它会不断的进化和发展,因为这个逻辑的‘箱’会不断地被要求扩充和完善。由此思路推演,我们就不难预测出未来我们需要努力的方向和成功机会可能存在的地方—— !

 

C#装箱与拆箱的研究的更多相关文章

  1. Java暗箱操作之自动装箱与拆箱

    我以前在写Android项目的时候,估计写得最多最熟练的几句话就是: List<Integer> list = new ArrayList<Integer>(); list.a ...

  2. [转]JAVA自动装箱和拆箱

    http://www.cnblogs.com/dolphin0520/p/3780005.html 1.Java数据类型 装箱和拆箱之前,我们先来了解一下Java的基本数据类型. 在Java中,数据类 ...

  3. 6个重要的.NET概念: - 堆栈,堆,值类型,引用类型,装箱和拆箱(转)

    今天在Code Project上面看到一篇文章<6 important .NET concepts: - Stack, heap, Value types, reference types, b ...

  4. 浅谈.NET中的类型和装箱、拆箱原理

    谈到装箱拆箱,大概的意思就是值类型和引用类型的相互转换呗---值类型到引用类型叫装箱,反之则叫拆箱.这当然没有问题,可是你只知道这么多,那么建议你花点时间看看楼主这篇文章 1. .NET中的类型 为了 ...

  5. 【深入理解CLR】2:细谈值类型的装箱和拆箱

    装箱 总所周知,值类型是比引用类型更“轻型”的一种类型,因为它们不作为对象在托管堆中分配,不会被垃圾回收,也不通过指针来引用.但在许多情况下,都需要获取对值类型的一个实例的引用.例如,假定要创建一个A ...

  6. C#基础回顾(二)—页面值传递、重载与重写、类与结构体、装箱与拆箱

    一.前言 -孤独的路上有梦想作伴,乘风破浪- 二.页面值传递 (1)C#各页面之间可以进行数据的交换和传递,页面之间可根据获取的数据,进行各自的操作(跳转.计算等操作).为了实现多种方式的数据传递,C ...

  7. 【译】.NET中六个重要的概念:栈、堆、值类型、引用类型、装箱和拆箱

    为何要翻译 一来是为了感受国外优秀技术社区知名博主的高质量文章,二来是为了复习对.NET技术的基础拾遗达到温故知新的效果,最后也是为了锻炼一下自己的英文读写能力.因为是首次翻译英文文章(哎,原谅我这个 ...

  8. java 自动装箱自动拆箱

    1.Java数据类型 在介绍Java的自动装箱和拆箱之前,我们先来了解一下Java的基本数据类型. 在Java中,数据类型可以分为两大种,Primitive Type(基本类型)和Reference ...

  9. Java 自动装箱与拆箱

    Java 自动装箱与拆箱(Autoboxing and unboxing)   什么是自动装箱拆箱 基本数据类型的自动装箱(autoboxing).拆箱(unboxing)是自J2SE 5.0开始提供 ...

随机推荐

  1. Educational Codeforces Round 48 (Rated for Div. 2) D 1016D Vasya And The Matrix (构造)

    D. Vasya And The Matrix time limit per test 2 seconds memory limit per test 256 megabytes input stan ...

  2. 模型事件注意点,before_delete、after_delete、before_write、after_write、before_update、after_update、before_insert、after_insert

    模型类支持before_delete.after_delete.before_write.after_write.before_update.after_update.before_insert.af ...

  3. JMeter - 如何测试REST API / 微服务

    概述: 有许多方法和工具可用于测试REST API.当我需要测试REST API时,在查看了各种工具和选项之后,由于以下原因,我选择了JMeter. JMeter是免费和开源的. JMeter可以从C ...

  4. 由奇葩cookie导致服务器500来认识cookie

    问题:cookie中文会导致服务器报500错误. 一:cookie的特点 1.以键值对的形式出现的,比如:a=b;b=c 2.中文的值需要转义 cookie的例子 <!DOCTYPE html& ...

  5. Javascript专题(三)c.各种轮播--上下滚动轮播(面向对象版本)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. 用servlet获取IP等信息

    Locale languageType=request.getLocale();//获取用户语言 String localIp=request.getLocalAddr();//获取本地ip int  ...

  7. mycat学习日记:关于联表查询

    https://www.cnblogs.com/toulon/p/4832895.html 在使用数据库中间件之前,我就想到分库分表的操作对于联表操作可能会显得非常复杂.因为如果数据是分片存储的,如果 ...

  8. Java多线程与并发——线程生命周期和线程池

    线程生命周期:  线程池:是预先创建线程的一种技术.线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中,然后对这些资源进行复用.减少频繁的创建和销毁对象. java里面线程池的顶级接口是E ...

  9. Java文件与io——字节流

    FileOutputStream用于写入诸如图像数据之类的原始字节的流 字节输出流:OutputStream 此抽象类表示输出字节流的所有类的超类.(写) 字节输入流:InputStream(读) p ...

  10. 功放TAS5719耳机POP noise分析 对策

    驱动功放时,遇到POP noise 是常见的问题,以前遇到此问题,首先想到的对策就是mute the output.本人一直来对此种解法感觉不爽,心里不快.最近遇到开机时耳机有很强的POP,决定好好研 ...