C#中的拆箱与装箱

1.什么是拆箱和装箱

在C#中,值类型是直接将数据存储在栈空间中;而引用类型是将数据存储在堆空间中,同时在栈空间中存储一个对该数据的引用。那么如果将一个值类型转换为一个它实现的某个接口或object会发生什么?结果必然是对一个存储位置的引用,且这个存储位置表面上存储的是这个引用类型的实例,实际上则是存储的值类型的值。这个转换称为装箱。相反的过程称为拆箱。

int number = 10;  
// 装箱  
object obj = number;  
// 拆箱  
number = (int) obj;

装箱(从值类型转换到引用类型)需要经历如下几个步骤:

  • 首先在堆上分配内存。这些内存主要用于存储值类型的数据。
  • 接着发生一次内存拷贝动作,将当前存储位置的值类型数据拷贝到堆上分配好的位置。
  • 最后返回对堆上的新存储位置的引用。

拆箱(从引用类型转换为值类型)的步骤则相反:

  • 首先检查已装箱的值的类型兼容目标类型。
  • 接着发生一次内存拷贝动作,将堆中存储的值拷贝到栈上的值类型实例中。
  • 最后返回这个新的值。

2.频繁拆装箱导致性能问题

由于拆箱和装箱都会涉及到一次内存拷贝动作,因此频繁地进行拆装箱会大幅影响性能。不同于拆箱需要进行强制类型转换,装箱由于包含隐式的类型转换而更容易被忽视。
下面来看一个例子:

int count = 10;  
ArrayList list = new ArrayList();  
list.Add(count);  
list.Add(20);  
int sum = (int) list[0] + (int) list[1];  
Console.Write("结果为:{0}",sum);

上面这段代码我们通过查看CIL就能发现,其总共执行了3次装箱和2次拆箱操作。

  • 首先是ArrayList的Add方法,其参数为object类型,因而添加int类型的对象时会执行装箱操作,总共执行了2次。
  • 当计算集合中的元素之和时,由于集合中存储的是object类型的数据,因此需要进行强制类型转换,造成了拆箱操作,总共执行了2次。
  • 当调用Console.Write方法时,它的签名为 void Write(string format, object arg),包含从int类型到object类型的装箱操作,共1次。

因此要注意这种“不起眼”的隐式类型转换。

3.避免可变值类型

我们来看一段代码:

struct Vector3
{
	public float X { get; set; }
	public float Y { get; set; }
	public float Z { get; set; }

	public void MoveTo(int x, int y, int z)
	{
		X = x;
		Y = y;
		Z = z;
	}
}
Vector3 vector3 = new Vector3() {X = 10, Y = 20, Z = 30};  
object objVector3 = vector3;  
((Vector3) objVector3).MoveTo(40,50,60);  
Console.WriteLine($"objVector3 is ({((Vector3) objVector3).X},{((Vector3) objVector3).Y},{((Vector3) objVector3).Z})");

按照惯性思维我们可能认为输出为objVector3 is (40,50,60),但实际的输出却是objVector3 is (10,20,30)。为什么呢?

首先,Vector3是值类型,我们创建了一个实例,并将其赋值为(10,20,30)。然后将其赋值给一个object类型的变量,此时编译器会进行一次装箱,会拷贝一份数据到堆空间中,并返回对该空间的引用。而为了调用MoveTo方法,编译器又对objVector3进行了拆箱,并创建了一份值的拷贝。调用MoveTo方法时,改变的是这份拷贝的值,而原引用指向的堆空间中的数据并没有被修改。

从这个例子可以看出,可变值类型容易让人产生迷惑,因为往往修改的是值的拷贝而非本体。因此要尽量避免设计这种可变值类型。


参考文献:
[1]马克·米凯利斯.C#8.0本质论[M].机械工业出版社.

你可能感兴趣的:(C#,c#,开发语言)