原文:[CLR via C#]5.3 值类型的装箱和拆箱

  在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制。

  下面总结了对值类型的一个实例进行装箱操作时内部发生的事:
  1)在托管堆中分配好内存。分配的内存量是值类型的各个字段需要的内存量加上托管堆上的所有对象都有的两个额外成员(类型对象指针同步块索引)需要的内存量。
  2)值类型的字段复制到新的分配的堆内存。
  3)返回对象的地址。现在,这个地址是对一个对象的引用,值类型现在是一个引用类型。
  拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低得多。拆箱其实就是一个获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。事实上,指针指向的是已装箱实例中的未装箱部分。所以,和装箱不同,拆箱不要求在内存中复制字节。还有一个重点就是,拆箱之后,往往会紧接着发生一次字段的复制操作。
  一个已装箱的值类型实例在拆箱时,内部会发生下面这些事情。
  1.如果包含了"对已装箱值类型实例的引用"的变量为null,就抛出一个NullReferenceException异常。
  2.如果引用指向的对象不是所期待的值类型的一个已装箱实例,就抛出一个InvalidCastException异常。
  上面第二条意味着一下代码不会如你预期的那样工作:
  1. public static void Main(){
  2. Int32 x = ;
  3. Object o = x;
  4. Int16 y = (Int16) o;//抛出异常
  5. }

  在对一个对象进行拆箱的时候,只能将其转型为原始未装箱时的值类型——Int32,下面是正确的写法:

  1. public static void Main(){
  2. Int32 x = ;
  3. Object o = x;      //对x进行装箱,o引用已装箱的对象 
  4. Int16 y = (Int16) (Int32) o; //先拆箱为正确的类型,在进行装箱
  5. }

  前面说过,在进行一次拆箱后,经常会紧接着一次字段的复制。以下演示了拆箱和复制操作:

  1. public static void Main() {
  2. Point p = new Point(); //栈变量
  3. p.x = p.y = ;
  4. object o = p; //对p进行装箱,o引用已装箱的实例
  5. p = (Point) o; //对o进行拆装,将字段从已装箱的实例复制到栈变量
  6. }

  在最后一行,C#编译器会生成一条IL指令对o进行拆箱,并生成另一条IL指令将这些字段从堆复制到基于栈的变量p中。

  再看看一下代码:

  1. public static void Main() {
  2. Point p = new Point(); // 栈变量
  3. p.x = p.y = ;
  4. object o = p; // 对p进行装箱,o引用已装箱的实例
  5.  
  6. // 将Point的x字段变成2
  7. p = (Point) o; // 对o进行拆装,将字段从已装箱的实例复制到栈变量
  8. p.x = ; // 更改变量的状态
  9. o = p; // 对p进行装箱,o引用已装箱的实例
  10. }

  最后三行代码唯一的目的就是将Point的x字段从1变成2.为此,首先要执行一次拆箱,在执行一次字段复制,在更改字段(在栈上),最后执行一次装箱(从而在托管堆上创建一个全新的已装箱实例)。希望你能体会到装箱和拆箱/复制操作对应用程序性能的影响。

  在看个演示装箱和拆箱的例子:  

  1. private static void Main(string[] args)
  2. {
  3. Int32 v = ; // 创建一个伪装箱的值类型变量
  4. Object o = v; // o引用一个已装箱的、包含值5的Int32
  5. v = ; // 将未装箱的值修改成为123
  6. Console.WriteLine(v + "," + (Int32)o); //显示"123,5"
  7. }

  你可以看出上述代码进行了几次装箱操作?如果说是3次,你会不会意味呢?我们来看下生成的IL代码。  

  1. .method private hidebysig static void Main(string[] args) cil managed
  2. {
  3. .entrypoint
  4. //代码大小
  5. .maxstack
  6. .locals init (
  7. [] int32 num,
  8. [] object obj2)
  9. L_0000: nop
  10.  
  11. // 将5加载到v中
  12. L_0001: ldc.i4.
  13. L_0002: stloc.
  14.  
  15. // 对v进行装箱,将引用指针存储到o中
  16. L_0003: ldloc.
  17. L_0004: box int32
  18. L_0009: stloc.
  19.  
  20. // 将123加载到v中
  21. L_000a: ldc.i4.s 0x7b
  22. L_000c: stloc.
  23.  
  24. // 对v进行装箱,并将指针保留在栈上以进行Concat(连接)操作
  25. L_000d: ldloc.
  26. L_000e: box int32
  27.  
  28. // 将字符串加载到栈上以执行Concat操作
  29. L_0013: ldstr ","
  30.  
  31. // 对o进行拆箱:获取一个指针,它指向栈上的Int32的字段
  32. L_0018: ldloc.
  33. L_0019: unbox.any int32
  34.  
  35. // 对Int32进行装箱,并将指针保留在栈上以进行Concat(连接)操作
  36. L_001e: box int32
  37.  
  38. // 调用Concat
  39. L_0023: call string [mscorlib]System.String::Concat(object, object, object)
  40. // 将从Concat放回的字符串传给WriteLine
  41. L_0028: call void
  42. [mscorlib]System.Console::WriteLine(string)
  43. L_002d: nop
  44.  
  45. // 从Main返回
  46. L_002e: ret
  47. }

  提示:主要原因是在Console.WriteLine方法上。

  Console.WriteLine方法要求获取一个String对象,为了创建一个String对象,C#编译器生成的代码来调用String对象的静态方法Concate。该方法有几个重载的版本,唯一区别就是参数数量,在本例中需要连接三个数据项来创建一个字符串,所以编译器会选择以下Concat方法来调用:

  1. public static String Concat(Objetc arg0, Object arg1, Onject arg2);

  所以,如果像下面写对WriteLine的调用,生成的IL代码将具有更高的执行效率:

  1. Console.WriteLine(v + "," + o); //显示"123,5"

  这只是移除了变量o之前的(Int32)强制转换。就避免了一次拆箱和一次装箱。

  我们还可以这样调用WriteLine,进一步提升上述代码的性能:

  1. Console.WriteLine(v.ToString() + "," + o); //显示"123,5"

  现在,会为未装箱的值类型实例v调用ToString方法,它返回一个String。String类型已经是引用类型,所以能直接传给Concat方法,不需要任何装箱操作。

  下面在演示一个装箱和拆箱操作:  

  1. private static void Main(string[] args)
  2. {
  3. Int32 v = ; // 创建一个伪装箱的值类型变量
  4. Object o = v; // o引用一个已装箱的、包含值5的Int32
  5. v = ; // 将未装箱的值修改成为123
  6. Console.WriteLine(v) //显示"123"
  7. v = (Int32) o; //拆箱并将o复制到v
  8. Console.WriteLine(v); //显示"5"
  9. }

  上述代码发生了多少次装箱呢?答案是一次。因为System.Console类定义了获取一个Int32作为参数的WriteLine方法的重载版本:  

  1. public static String Concat(Int32 value);

  在WriteLine方法内部也许会发生装箱操作,但这已经不是我们能控制的。我们已经尽可能地从自己的代码中消除了装箱操作。

  最后,如果知道自己写的代码会造成编译器反复对一个值类型进行装箱,请改用手动方式对值类型进行装箱。

  对象相等性和同一性。System.Object类型提供了一个名为Equals的虚方法,它的作用是在两个对象包含相同的值得前提下返回true。如:

  1. public class Object{
  2. publick virtual Boolean Equals(Object obj) {
  3. //如果两个引用指向同一个对象,它们肯定包含相同的值
  4. if ( this == obj ) return true;
  5. //假定对象不包含相同的值
  6. return false;
  7. }
  8. }

  对于Object的Equals方法的默认实现来说,它实现的实际是同一性,而非相等性。

  下面展示了如何在内部正确实现一个Equals方法。
  1)如果obj实参为null,就返回false,因为在调用非静态的Equals方法时,this所标识的当前对象显然不为null.
  2)如果this和obj实参引用同一个对象,就返回true。在比较包含大量字段的对象时,这一步有助性能提升。
  3)如果this和obj实参引用不同类型的对象,就返回false。一个String对象显然不等于一个FileStream对象。
  4)针对类型定义的每个实例字段,将this对象中的值与obj对象中的值进行比较。任何字段不相等,就返回false。
  5)调用基类的Equals方法,以便比较它定义的任何字段。如果基类的Equals方法返回false,就返回false;否则返回true;
例如:
  1. public class Object{
  2. public virtual Boolean Equals(Object obj) {
  3. //要比较的对象不能为null
  4. if (obj == null ) return false;
  5. //如果对象类型不同,则肯定不相等
  6. if (this.GetType() != obj.GetType()) return false;
  7. //如果对象属于相同的类型,那么在它们所有字段都匹配的前提下返回true
  8. //由于System.Object没有定义任何字段,所以字段是匹配的
  9. return true;
  10. }
  11. }

  由于,一个类型能重写Object的Equals方法,所以不能再调用这个Equals方法来测试同一性。为了修正这一问题,Object提供了一个静态方法ReferenEquals,其原型如下:

  1. public class Object{
  2. public static Boolean ReferenceEquals(Object objA , Object objB) {
  3. retuen ( onjA == objB );
  4. }
  5. }

  如果想检查同一性,务必调用ReferenceEquals,而不应该使用C#的== 操作符,因为==操作符可能被重载。

  System.ValueType(所有值类型的基类)重写了Object的Equals方法,并进行了正确的实现来执行值得相等性检查。
 

[CLR via C#]5.3 值类型的装箱和拆箱的更多相关文章

  1. CLR via 笔记 5.3 值类型的装箱和拆箱

    1.装箱 为了将一个值类型转换成一个引用类型,要使用一个名为装箱(Boxing)的机制. 1.在托管堆中分配好内存.分配的内存量是值类型的各个字段需要的内存量加上托管堆的所有对象都有的两个额外成员(类 ...

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

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

  3. 【.Net基础二】浅谈引用类型、值类型和装箱、拆箱

    目前在看CLR via C#,把总结的记下来,索性就把他写成一个系列吧. 1.[.Net基础一] 类型.对象.线程栈.托管堆运行时的相互关系 2.[.Net基础二]浅谈引用类型.值类型和装箱.拆箱 引 ...

  4. [CLR via C#]值类型的装箱和拆箱

    我们先来看一个示例代码: namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Array ...

  5. 读经典——《CLR via C#》(Jeffrey Richter著) 笔记_值类型的装箱和拆箱(二)

    [注意]:如果知道自己写的代码会造成编译器反复对一个值类型进行装箱,请改成用手动方式对值类型进行装箱. [好处]:代码会变得更小.更快. [例子]: using System; public seal ...

  6. [C#] 类型学习笔记一:CLR中的类型,装箱和拆箱

    在学习.NET的时候,因为一些疑问,让我打算把.NET的类型篇做一个总结.总结以三篇博文的形式呈现. 这篇博文,作为三篇博文的第一篇,主要探讨了.NET Framework中的基本类型,以及这些类型一 ...

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

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

  8. .NET六大剑客:栈、堆、值类型、引用类型、装箱和拆箱

    .NET六大剑客:栈.堆.值类型.引用类型.装箱和拆箱 一.“堆”,“栈”专区 这两个字我相信大家太熟悉了,甚至于米饭是什么?不知道...“堆”,“栈”是什么?哦,这个知道... 之前我也写过一篇堆栈 ...

  9. C#基础知识系列二(值类型和引用类型、可空类型、堆和栈、装箱和拆箱)

    前言 之前对几个没什么理解,只是简单的用过可空类型,也是知道怎么用,至于为什么,还真不太清楚,通过整理本文章学到了很多知识,也许对于以后的各种代码优化都有好处. 本文的重点就是:值类型直接存储其值,引 ...

随机推荐

  1. Eclipse热键

    Eclipse编辑功能很强大.掌握Eclipse快捷功能.高开发效率.Eclipse中有例如以下一些和编辑相关的快捷键.     1. [ALT+/]     此快捷键为用户编辑的好帮手.能为用户提供 ...

  2. 读书时间《JavaScript高级程序设计》四:BOM,客户端检测

    隔了一段时间,现在开始看第8章. 第8章:BOM BOM提供了很多对象,用于访问浏览器的功能.BOM的核心对象是window,它表示浏览器的一个实例. window对象是通过javascript访问浏 ...

  3. webclient下载文件 带进度条

    private void button1_Click(object sender, EventArgs e) { doDownload(textBox1.Text.Trim()); } private ...

  4. 【Android进阶】Activity和Fragement中onSaveInstanceState()的使用详解

    在activity(或者是fragement)被杀掉之前调用保存每个实例的状态,以保证该状态可以在onCreate(Bundle)或者onRestoreInstanceState(Bundle) (传 ...

  5. uva--165(邮资问题,dp)

    题意: 某国家发行k种不同面值的邮票,而且规定每张信封上最多仅仅能贴h张邮票. 公式n(h,k)表示用从k中面值的邮票中选择h张邮票,能够组成面额为连续的1.2.3,--n. n是能达到的最大面值之和 ...

  6. 开发随笔——NOT IN vs NOT EXISTS

    原文:开发随笔--NOT IN vs NOT EXISTS 原文出处: http://blog.csdn.net/dba_huangzj/article/details/31374037  转载请引用 ...

  7. 也可以看看GCD(杭州电2504)(gcd)

    也可以看看GCD Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total S ...

  8. unity3d 各功能的运行秩序,打回来,订购,的次数

    Update 当MonoBehaviour启用时,其Update在每一帧被调用. 仅调用一次(每帧) LateUpdate 当Behaviour启用时,  每帧调用一次: FixedUpdate 当M ...

  9. php中国的垃圾问题

    header这条线加,这是解决中国乱码的问题. 版权声明:本文博主原创文章,博客,未经同意不得转载.

  10. c# 16进制显示转化

    非原创. 接收16进制数据,在TextBox委托显示: private void readPortandShow() { char[] HexChar = { '0', '1', '2', '3', ...