C# 装箱 和 拆箱 研究 我认为最好的

    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代码察看工具)为此我们先来看看以下的代码:

  (注释下: Visual studio 2010的IL DASM 在Visual Studio 2010的Microsoft Windows SDK Tools下面。)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BoxAndUnbox
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a value
            double dubBox = 77.77;

            // Box the value of the variable to a reference
            object objBox = dubBox;

            Console.WriteLine("The value is '{0}' and the Boxed is {1}", dubBox, objBox.ToString());
        }
    }
}

查看IL DASM代码

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       42 (0x2a)
  .maxstack  3
  .locals init ([0] float64 dubBox,
           [1] object objBox)
  IL_0000:  nop
  IL_0001:  ldc.r8     77.769999999999996
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  box        [mscorlib]System.Double
  IL_0011:  stloc.1
  IL_0012:  ldstr      "The value is '{0}' and the Boxed is {1}"
  IL_0017:  ldloc.0
  IL_0018:  box        [mscorlib]System.Double
  IL_001d:  ldloc.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 Program::Main


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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BoxAndUnbox
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a value
            double dubBox = 77.77;

            // Box the value of the variable to a reference
            object objBox = dubBox;

            // Unbox
            double dubUnBox = (double)objBox;

            Console.WriteLine("The value is '{0}' and the Boxed is {1}", dubBox, objBox.ToString());
        }
    }
}

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

再看看IL DASM代码

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       49 (0x31)
  .maxstack  3
  .locals init ([0] float64 dubBox,
           [1] object objBox,
           [2] float64 dubUnBox)
  IL_0000:  nop
  IL_0001:  ldc.r8     77.769999999999996
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  box        [mscorlib]System.Double
  IL_0011:  stloc.1
  IL_0012:  ldloc.1
  IL_0013:  unbox.any  [mscorlib]System.Double
  IL_0018:  stloc.2
  IL_0019:  ldstr      "The value is '{0}' and the Boxed is {1}"
  IL_001e:  ldloc.0
  IL_001f:  box        [mscorlib]System.Double
  IL_0024:  ldloc.1
  IL_0025:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_002a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::Main


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

 

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BoxAndUnbox
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a value
            double dubBox = 77.77;

            // Box the value of the variable to a reference
            object objBox = dubBox;

            // Unbox
            double dubUnBox = (double)objBox;

            object objUnBox = dubUnBox;

            Console.WriteLine("The value is '{0}' and the Boxed is {1}", objBox, objUnBox);
        }
    }
}

再来看看IL DASM代码

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       46 (0x2e)
  .maxstack  3
  .locals init ([0] float64 dubBox,
           [1] object objBox,
           [2] float64 dubUnBox,
           [3] object objUnBox)
  IL_0000:  nop
  IL_0001:  ldc.r8     77.769999999999996
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  box        [mscorlib]System.Double
  IL_0011:  stloc.1
  IL_0012:  ldloc.1
  IL_0013:  unbox.any  [mscorlib]System.Double
  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 Boxed is {1}"
  IL_0025:  ldloc.1
  IL_0026:  ldloc.3
  IL_0027:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_002c:  nop
  IL_002d:  ret
} // end of method Program::Main


这里就没有BOX的过程了。

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

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

最后说说

装箱/拆箱对执行效率的影响
显然,从原理上可以看出,装箱时,生成的是全新的引用对象,这会有时间损耗,也就是造成效率降低。
那该如何做呢?
首先,应该尽量避免装箱。
比如上例2的两种情况,都可以避免,在第一种情况下,可以通过重载函数来避免。第二种情况,则可以通过泛型来避免。
当然,凡事并不能绝对,假设你想改造的代码为第三方程序集,你无法更改,那你只能是装箱了。
对于装箱/拆箱代码的优化,由于C#中对装箱和拆箱都是隐式的,所以,根本的方法是对代码进行分析,而分析最直接的方式是了解原理结何查看反编译的IL代码。比如:在循环体中可能存在多余的装箱,你可以简单采用提前装箱方式进行优化。

通过泛型来避免这个在泛型编程里就讲到O(∩_∩)O哈! 

 

伪python爱好者,正宗测试实践者。
原文地址:https://www.cnblogs.com/herbert/p/1749876.html