本文参考 《CLR via C#》、搜索引擎
值类型比引用类型要 “轻” 那么一点,值类型使用的时候也非常的方便,它们
不作为对象在托管推中分配,没有被当作垃圾回收掉,也不能通过指针进行引用
。但许多的时候都需要对值类型进行实例的引用,这就是我们所常说的 “装箱
”,当然,有装箱就有拆箱,下面就让我们一起来了解一下,值类型与引用类型之间的那些事儿吧 . . .
.
装箱是一个非常浪费性能的操作,在学习过程中,我们尽量避免这种操作,养成好的习惯 . . .
文章目录
-
装箱与拆箱
-
实例讲解程序中的装箱与拆箱机制
-
平常注意的两个操作点
.
装箱与拆箱
—— 装箱
将 值类型
转换成 引用类型
就要使用到 装箱
机制,装箱的时候会发生如下几种情况:
- 在托管堆中分配内存
- 值类型的字段复制到新分配的堆内存
- 返回对象地址。 该地址是对象引用,值类型 --> 引用类型
下面这个图演示了 装箱 的过程:
C# 编译器会自动生成对值类型实例进行装箱所需的 IL 代码,下面我会进行演示 . . .
.
—— 拆箱
有装箱就有拆箱,那么怎么样才能完成拆箱呢? 完成拆箱主要有两步:
- 获取已装箱的 值类型在堆中的各个字段的地址(
拆箱
) - 将字段包含的值从堆中复制到基于栈的值类型实例中
.
介绍完装箱与拆箱的概念原理之后,我们下面就来研究一下他们的代码实现吧
进行装箱的时候,我们感觉就像直接复制了一样,实则 像上面那个装箱图一样做了许多事情,在进行拆箱的时候,感觉就像强制类型转换一样,但也不然 . . .
短短的两行代码,却直接演示了 装箱与拆箱. . .
这里需要注意的是,我们进行拆箱的时候,如果引用的对象不是所需值类型的已装箱实例,就会抛出异常,比如下面这种情况:
如果我们一定要使用 Int16 来进行拆箱,那么我们可以使用下面的这种写法:
对对象进行拆箱时,只能转型为最初未装箱的值类型 Int32,之后再进行强制转换为 Int16 . . .
上面我们提到过,进行拆箱时会进行一次字段复制,那么我们输出 newValue的值 应该是 42
,事实也如此 . . .
.
它的结果是:123,5
因为 o 引用的是已经装箱的 v,不管我们如何的修改未装箱的 v,都不会影响到它 . . .
那么它到底进行了几次装箱呢? 答案是三次,是不是有点小意外 ^ _ ^,我们来看一看这个程序生成的 IL 代码,我们可以通过 ILDASM 工具进行查看:
注意我用红色框起来的部分,我们使用 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 快乐
文中可能有许多不对的地方,欢迎前辈纠正,谢谢大家 ^ _ ^