CSharp笔记-装箱与拆箱

官方指南翻译

官方指南: Boxing and Unboxing (C# Programming Guide)

装箱是把一个值类型转换为object类型或转换为任何实现了该值类型接口的类型的过程.当CLR装箱一个值类型时,它将这个值封装进一个System.Object并且存在堆中.拆箱将这个值从这个object中取出来.装箱是隐式的,拆箱是显式的.装箱和拆箱的概念是C#类型系统统一视图的基础.C#类型系统中,一任何类型的值都可以被当做object.

在下面的例子里,int值i装箱,并赋值给objecto

1
2
3
int i = 123;
// 下面一行装箱了i.
object o = i;

然后objecto可以拆箱并赋值给变量i

1
2
o = 123;
i = (int)o; // 拆箱

下面的例子解释了装箱在C#中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// String.Concat 例子
// 42和true必须被装箱
Console.WriteLine(String.Concat("Answer", 42, true));

// List 例子
// 新建一个objects的list存放各种各样类型的元素
List<object> mixedList = new List<object>();

// 往这个list中天际一个string
mixedList.Add("First Group:");

// 往这个list中添加一些int
for (int j = 1; j < 5; j++)
{
// 每个添加进这个list的j都会被装箱
mixedList.Add(j);
}

// 添加另一个string和更多int
mixedList.Add("Second Group:");
for (int j = 5; j < 10; j++)
{
mixedList.Add(j);
}

// 显示这个list中的所有元素. 用var声明循环中的所有变量, 然后编译器会分配它的类型
foreach (var item in mixedList)
{
Console.WriteLine(item);
}

// 下面的循环将第一批的int的平方加起来
// 这些list的元素是objects,在被拆箱之前不能乘和加
// 这个拆箱必须是显式的
var sum = 0;
for (var j = 1; j < 5; j++)
{
// 下面的语句会引起一个编译错误:
// Operator '*' cannot be applied to operands of type 'object' and 'object'.
//sum += mixedList[j] * mixedList[j]);

// 当列表元素拆箱后,计算不会引起编译错误
sum += (int)mixedList[j] * (int)mixedList[j];
}

// 显示的结果是 30, 1 + 4 + 9 + 16.
Console.WriteLine("Sum: " + sum);

// Output:
// Answer42True
// First Group:
// 1
// 2
// 3
// 4
// Second Group:
// 5
// 6
// 7
// 8
// 9
// Sum: 30

性能

因为涉及到一个简单的分配, 所以装箱和拆箱在计算上是昂贵的. 当一个值类型被装箱时, 一个新的object就会被指派和创建. 在较小的程度上, 拆箱需要的花费在计算上也是很昂贵. 更多信息请浏览性能

装箱

装箱用来将值类型存到垃圾回收(GC)堆中. 装箱隐式地将值类型转换为object类型或转换为任何实现了该值类型接口的类型. 将一个值类型装箱会在堆中指派一个object实例并将这个值拷贝进这个新的object.

考虑下下面这个值类型变量的声明:

1
int i = 123;

下面的语句隐式地对变量i进行了拆箱:

1
2
// 拆箱将i的值拷贝进object o
object o = i;

上面语句的结果是,在栈上创建一个对象o的引用, 引用了在堆上的int类型的值. 这个值是一个分配到变量i上的值类型的拷贝. io这两个变量的区别可以用下面这张装箱变换的图来形象的解释:
20190422140244.png

装箱也可以像下面这样显式的执行, 不过显式的装箱永远不会用到:

1
2
int i = 123;
object o = (object)i; // 显式的装箱

描述

这个例子用装箱将一个int变量i转换成一个objecto. 然后,如果存放在变量i中的值从123变为456… 下面的例子表明了,圆本
的值类型和装箱后的object使用不同的内存地址, 所以可以存储不同的值.

例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TestBoxing
{
static void Main()
{
int i = 123;

// 装箱将i的值拷贝进object o
object o = i;

// 改变i的值
i = 456;

// i的改变不会影响到o中值的变化
System.Console.WriteLine("The value-type value = {0}", i);
System.Console.WriteLine("The object-type value = {0}", o);
}
}
/* 输出:
The value-type value = 456
The object-type value = 123
*/

拆箱

拆箱显式地将object类型或任何实现了该值类型接口的类型转换为一个值类型. 一个拆箱操作由下面两部分组成:

  • 确定这个object实例是一个给定值类型的装箱.
  • 将这个实例中的值拷贝给一个值类型变量.

下面的语句展示了装箱和拆箱操作:

1
2
3
int i = 123;      // 一个值类型
object o = i; // 装箱
int j = (int)o; // 拆箱

下面的图是上面语句的演示:

20190422141656.png

为了确保拆箱在运行时能成功, 被拆箱的东西必须是一个之前被装箱创建的object的引用. 尝试去拆箱null会引发一个NullReferenceException.尝试拆箱一个不相容的类型会引发一个InvalidCastException

例子

下面的例子示范了一个不正确的引发了InvalidCastException的拆箱的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TestUnboxing
{
static void Main()
{
int i = 123;
object o = i; // 隐式装箱

try
{
int j = (short)o; // 尝试拆箱

System.Console.WriteLine("Unboxing OK.");
}
catch (System.InvalidCastException e)
{
System.Console.WriteLine("{0} Error: Incorrect unboxing.", e.Message);
}
}
}

上面的程序会输出:
Specified cast is not valid. Error: Incorrect unboxing.
如果你将int j = (short) o;改为int j = (int) o;, 这个转换就会成功, 会输出Unboxing OK.