C#装箱与拆箱的研究

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

观者也许曾无数次的使用过诸如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

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

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

时间: 2024-08-27 13:36:16

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

Java语言暗箱操作之自动装箱与拆箱

我以前在写Android项目的时候,估计写得最多最熟练的几句话就是: List<Integer> list = new ArrayList<Integer>(); list.add(1); //把一个整数加入到集合中 int i = list.get(0); //从集合中取出元素 ArrayList用起来是多么的顺手!当时我只知道尖括号<>里面只能加入大写字母开头的Object类型,不能加入int.char.double这些原始类型,至于原因没研究过,这么规定就这么用呗

[转]JAVA自动装箱和拆箱

1.Java数据类型 装箱和拆箱之前,我们先来了解一下Java的基本数据类型. 在Java中,数据类型可以分为两大种,Primitive Type(基本类型)和Reference Type(引用类型).基本类型的数值不是对象,不能调用对象的toString().hashCode().getClass().equals()等方法.所以Java提供了针对每种基本类型的包装类型.如下: Java基本数据类型 INDEX 基本类型  大小 数值范围 默认值 包装类型 1 boolean    --- t

java 装箱和拆箱

每次看到某篇博客里面说要静下心来欣赏源码,我都很感慨,我的耐心没人家好.是真的要静下心来读源码,因为说不定就像作者说的那样 “说不定你就发现了一个优秀的设计呢!!!” 才不久就被同事说了,学知识的时候不要看野史,要看官方.我还是做不到.官方是一定要看的,但是野史还是照样翻,因为我觉得很多野史写的很生动,甚至有滋有味,有声有色,能从作者的言词中感受到人家的认真,不知不觉中能成为自己的榜样. 都是同行,为什么人家就能那么优秀,我要向其学习. 题外话 今天早上在学习公司代码,然后准备学习下MVP框架,

深入剖析Java中的装箱和拆箱

阅读目录 一.什么是装箱?什么是拆箱?二.装箱和拆箱是如何实现的三.面试中相关的问题 自动装箱和拆箱问题是Java中一个老生常谈的问题了,今天我们就来一些看一下装箱和拆箱中的若干问题.本文先讲述装箱和拆箱最基本的东西,再来看一下面试笔试中经常遇到的与装箱.拆箱相关的问题. 回到顶部 一.什么是装箱?什么是拆箱? 我们知道 Java为每种基本数据类型都提供了对应的包装器类型,至于为什么会为每种基本数据类型提供包装器类型在此不进行阐述,有兴趣的朋友可以查阅相关资料.在Java SE5之前,如果要生成

转 C# 装箱和拆箱[整理]

1.      装箱和拆箱是一个抽象的概念 2.      装箱是将值类型转换为引用类型 :拆箱是将引用类型转换为值类型       利用装箱和拆箱功能,可通过允许值类型的任何值与Object 类型的值相互转换,将值类型与引用类型链接起来 例如: int val = 100; object obj = val; Console.WriteLine (“对象的值 = {0}", obj); 这是一个装箱的过程,是将值类型转换为引用类型的过程 int val = 100; object obj =

【c#】装箱与拆箱

从内存执行的角度来看,值类型的内存分配在线程的堆栈上,而引用类型的内存分配在托管堆上.因此从值类型向引用类型的转换,势必牵涉到数据的拷贝与指针引用等操作. 装箱操作,大致过程为:在托管堆中分配新对象的内存,将值类型的字段拷贝到该内存中,然后返回该对象的地址,这样就完成了从值类型到引用类型的转变:拆箱操作,则是获取已装箱对象中来自值类型部分字段的地址.装箱与拆箱并非完全对称的互逆操作,拆箱并不包含字段的拷贝. 概念雷区: 装箱与拆箱不是完全对等的互逆操作.从内存的角度上看,拆箱的性能开销远小于装箱

《Java中的自动装箱和拆箱功能.》

1 //Java中的自动装箱和拆箱功能. 2 class AutoboxingUnboxing 3 { 4 public static void main(String[] args) 5 { 6 //直接把一个基本类型变量赋给Interger对象. 7 Integer inObj = 5; 8 Object inObj1 = 6; 9 //直接把一个boolean类型的变量赋给一个Object类型的变量. 10 Object boolObj = true; 11 //直接把Integer对象赋

Java中的装箱与拆箱

在J2SE5.0后推出了自动装箱和拆箱的功能,以提高我们的开发效率,然而自动装箱和拆箱实际上是通过编译器来支持的(并非语言本身,或者说虚拟机),因而这种支持也隐藏了部分内部实质,再加上某些类的优化(比如Integer里面的缓存等,参看关于缓存节),就更加容易在特定的环境下产生问题,并且如果不知道原来还无法调试.以下先是简单的介绍了编译器对装箱和拆箱的实现,并根据实现简单介绍一下可能会遇到的几个问题. 在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行: Integ

值类型的装箱和拆箱

在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制. 下面总结了对值类型的一个实例进行装箱操作时内部发生的事: 1)在托管堆中分配好内存.分配的内存量是值类型的各个字段需要的内存量加上托管堆上的所有对象都有的两个额外成员(类型对象指针和同步块索引)需要的内存量. 2)值类型的字段复制到新的分配的堆内存. 3)返回对象的地址.现在,这个地址是对一个对象的引用,值类型现在是一个引用类型. 拆箱不是直接将装箱过程倒过来.拆箱的代价比装箱低得多.拆箱其实就是一个获取一个指针的过程,该