C# 装箱与拆箱机制详解

本文参考 《CLR via C#》、搜索引擎

值类型比引用类型要 “轻” 那么一点,值类型使用的时候也非常的方便,它们 不作为对象在托管推中分配,没有被当作垃圾回收掉,也不能通过指针进行引用。但许多的时候都需要对值类型进行实例的引用,这就是我们所常说的 “装箱”,当然,有装箱就有拆箱,下面就让我们一起来了解一下,值类型与引用类型之间的那些事儿吧 . . .
.
装箱是一个非常浪费性能的操作,在学习过程中,我们尽量避免这种操作,养成好的习惯 . . .


在这里插入图片描述

文章目录


.

装箱与拆箱

—— 装箱

值类型 转换成 引用类型 就要使用到 装箱 机制,装箱的时候会发生如下几种情况:

  • 在托管堆中分配内存
  • 值类型的字段复制到新分配的堆内存
  • 返回对象地址。 该地址是对象引用,值类型 --> 引用类型

下面这个图演示了 装箱 的过程:

在这里插入图片描述

C# 编译器会自动生成对值类型实例进行装箱所需的 IL 代码,下面我会进行演示 . . .

.

—— 拆箱

有装箱就有拆箱,那么怎么样才能完成拆箱呢? 完成拆箱主要有两步:

  • 获取已装箱的 值类型在堆中的各个字段的地址(拆箱
  • 将字段包含的值从堆中复制到基于栈的值类型实例

在这里插入图片描述

.

介绍完装箱与拆箱的概念原理之后,我们下面就来研究一下他们的代码实现吧

例如我们对一个 Int32 型 的数据进行装箱与拆箱:
在这里插入图片描述

进行装箱的时候,我们感觉就像直接复制了一样,实则 像上面那个装箱图一样做了许多事情,在进行拆箱的时候,感觉就像强制类型转换一样,但也不然 . . .

短短的两行代码,却直接演示了 装箱与拆箱. . .

这里需要注意的是,我们进行拆箱的时候,如果引用的对象不是所需值类型的已装箱实例,就会抛出异常,比如下面这种情况:
在这里插入图片描述
如果我们一定要使用 Int16 来进行拆箱,那么我们可以使用下面的这种写法:
在这里插入图片描述
对对象进行拆箱时,只能转型为最初未装箱的值类型 Int32,之后再进行强制转换为 Int16 . . .

上面我们提到过,进行拆箱时会进行一次字段复制,那么我们输出 newValue的值 应该是 42,事实也如此 . . .

.

下面我们看一个例子,其中进行了几个装箱呢?
在这里插入图片描述

它的结果是:123,5
因为 o 引用的是已经装箱的 v,不管我们如何的修改未装箱的 v,都不会影响到它 . . .

那么它到底进行了几次装箱呢? 答案是三次,是不是有点小意外 ^ _ ^,我们来看一看这个程序生成的 IL 代码,我们可以通过 ILDASM 工具进行查看:

  • 直接将.exe 文件插入其中即可:
    在这里插入图片描述
  • 生成的 IL 代码:
    在这里插入图片描述

注意我用红色框起来的部分,我们使用 Console.WriteLine 进行输出时,它把三个参数都当成 String型数据连接起来,然后输出 . . .

那么三次装箱是哪三次呢? 下面就是正确的答案,你知道吗:

- Object o = v; 				// 第一次装箱
- Console.WriteLine 中的 v  			// 第二次装箱
- Console.WriteLine 中的 (Int32)o   		// 拆箱后又进行装箱

之所以三个参数都是 String型,是因为 Console.WriteLine没有重载 Console.WriteLine(Int32,
String, Int32) 这个方法,所以Console.WriteLine 直接把它们都当成引用类型(String) 连接起来了 . .
.
大家可以细细的品 . . . ^ _ ^

.

装箱与拆箱的基本概念与代码介绍到此,下面我们来实践一下一个小程序,看看其中有多少的装箱拆箱,此外,我再次提醒,装箱很废性能,尽量避免这样的操作,但有的时候,我们又不得不去进行装箱,比如上面的那个有三次装箱的 Console.WriteLine,我们可以将它改成如下的样子:

在这里插入图片描述

这样子,这个例子就进行了 一次装箱, v.ToString 和 ", " 和 o 是一样的引用类型 . . .

. . .


.

实例讲解程序中的装箱与拆箱机制

当我们把值类型转化为接口类型也需要装箱操作的,下面这个例子就完美的体现出,如果看懂了,我们就真正的理解装箱与拆箱机制了,每一行 Console.WriteLine 代码我都加以注释 . . .

using System;

namespace BoxDemo
{ 
    class Program
    {
        static void Main(string[] args)
        {
            // 值类型,在栈上创建两个 Point实例
            Point p1 = new Point(10, 10);
            Point p2 = new Point(20, 20);

	    // 调用 ToString(虚方法) 不装箱 p1
            Console.WriteLine(p1.ToString());

	    // 调用 GetType(非虚方法)时,要对 p1 进行装箱
            Console.WriteLine(p1.GetType());                //Object

  	    // 调用 CompareTo 不装箱 p1
            // 调用的是重载过的 CompareTo
            Console.WriteLine(p1.CompareTo(p2));

 	    // 装箱 放到 c中
            IComparable c = p1;
            Console.WriteLine(c.GetType());

	    // 调用 CompareTo 不装箱 p1
            // 调用的是IComparable 实现的接口 CompareTo
            Console.WriteLine(p1.CompareTo(c));

	    // c 不装箱(引用 p1)    p2 装箱
            Console.WriteLine(c.CompareTo(p2));

	    // 对 c 拆箱,字段复制到 p2中
            p2 = (Point)c;
            Console.WriteLine(p2.ToString());
        }
    }

    internal struct Point : IComparable
    {
        private Int32 m_x, m_y;

	public Point(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }

	public override String ToString()
        {
            // 返回 Point,避免 ToString 装箱
            return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());
        }

	public Int32 CompareTo(Point other)
        {
            // 计算 Point的 哪个点 离 (0,0) 更远
            return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) -
                Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
        }

	// 实现接口中的 CompareTo
        public Int32 CompareTo(object obj)
        {
            if(this.GetType() != obj.GetType())
            {
                throw new ArgumentException("o is not a Point");    
            }
            
            // 调用类型安全的 CompareTo方法
            return CompareTo((Point)obj);   
        }
    }
}

.

平常注意的两个操作点

一、重复数据应避免多次装箱

例如下面,我们需要输出三个相同数据的值类型,但 Console.WriteLine 对他进行了三次装箱:

int v = 11;

Console.WriteLine("{0}, {1}, {2}", v, v, v);

解决办法:手动进行一次装箱,只进行一次装箱:

int v = 11;

Object o = v;
Console.WriteLine("{0}, {1}, {2}", o, o, o);

如果不知道这里的情况,请看上面这个相关的例子:
在这里插入图片描述

.

二、使用接口更改已经装箱值类型中的字段

首先,我们来测试一下没有使用接口的情况:

定义一个测试类:

在这里插入图片描述

测试代码:

在这里插入图片描述

测试的结果是:

在这里插入图片描述

这里,我们可能会想不到为什么最后的输出也是 (2,2)?

原因解析: 因为对引用类型进行拆箱时,它将已装箱 Point 中的字段复制到 线程栈上的一个 Point上面!但是已经装箱的 Point不会受这个 Change调用的影响,所以这就需要我们借助接口的使用了

.

接口的使用,改变已经装箱的值类型:

定义一个接口,并定义一个实现接口的类:

在这里插入图片描述

测试代码:

在这里插入图片描述

输出的结果为:

在这里插入图片描述

倒数第二个输出,造成这样的原因类似于:
在这里插入图片描述

最后一个输出,o 引用的已装箱 Point 转型为一个 IChangeBoxedPoint。这里不需要装箱,因为 o本来就是引用类型,它直接调用 Change 修改对应的数据 . . . 接口方法 Change 使我们能够完成我们想要的操作 . . .


5.20 快乐

文中可能有许多不对的地方,欢迎前辈纠正,谢谢大家 ^ _ ^

猜你喜欢

转载自blog.csdn.net/weixin_42100963/article/details/106219375