值类型和引用类型之间的搬运

前面我们认识了各种数据类型。这一篇讲一个让很多新手发懵的词——装箱(Boxing)和 拆箱(Unboxing)。别怕,它俩说白了就是「把值装进盒子、再从盒子取出来」两件事,看完这篇你就能彻底搞懂。

本篇目标:能说出什么是装箱、什么是拆箱,能自己写一段装箱拆箱的代码,并且知道拆箱时最容易踩的坑。


一、先补个前提:值类型 和 引用类型

拆箱装箱是「值类型」和「引用类型」之间的转换,所以得先分清这两类类型。就一句话。

用一个生活比喻分清两类类型

想象你有一笔钱(数据):

  • 值类型:把现金直接塞进自己的口袋。钱就在口袋里,要用直接掏。比如 intdoubleboolfloat,它们把「值本身」直接放在变量里。
  • 引用类型:口袋里的不是钱,是一把储物柜的钥匙。真正的钱放在储物柜里,你要用钱,得先拿着钥匙去开柜子。比如 stringobject,它们变量里放的是「地址」,真正的数据在别处。

一句话记:值类型装的是值本身引用类型装的是地址

分类 常见类型 变量里装的是
值类型 int double bool float long 值本身
引用类型 string object 自定义类 地址(钥匙)

还记得上一篇学的 intdouble 吗?它们都是值类型。而 string(字符串)是引用类型,object 是所有类型的「老祖宗」,它俩都是引用类型。记住这两个就够用了。


二、什么是装箱(Boxing)

装箱:把一个值类型(比如 int),装进 object 这个「盒子」里。因为 object 是所有类型的祖先,任何东西都能往里塞。

1
2
3
4
5
int value = 100;          // 值类型,装在变量里
object box = value; // 装箱:把 int 塞进 object 盒子里

Console.WriteLine(value); // 100
Console.WriteLine(box); // 100
装箱背后发生了什么?

装箱时,电脑做了两步:

  1. 在「堆」(储物柜区)里开辟一块新空间,专门放这个盒子。
  2. value 的值 100 复制一份,放进盒子里,再把盒子的地址交给 box

所以装箱不是「把 int 变成 object」,而是「复制一份值,包成一个 object」。原来的 value 还是 int,一点没变。

装箱是隐式的,也就是你什么都不用写,直接赋值 object box = value;,电脑自动帮你装箱。


三、什么是拆箱(Unboxing)

拆箱:把 object 盒子打开,把里面的值取出来,还原成原来的值类型。这次就必须写「强制转换」了。

1
2
3
4
object box = 100;         // 先装箱:盒子里装的是 int 类型的 100
int value = (int)box; // 拆箱:用 (int) 把盒子里的值取出来

Console.WriteLine(value); // 100
拆箱背后发生了什么?

拆箱也分两步:

  1. 先检查盒子里装的值,类型是不是 int
  2. 如果是,就把盒子里的 100 复制出来,交给 value;如果不是,直接报错(这个坑下一节重点讲)。

注意那个 (int) 不能省,它是「拆箱」的标志,告诉电脑我要按 int 来取。

装箱是隐式的(自动),拆箱是显式的(必须写 (类型))。这是它俩最明显的区别。


四、为什么要拆箱装箱?有什么用?

你可能想问:好端端的 int,干嘛非要装进 object 再取出来?答案是——有些地方只认 object,不认具体类型。

老集合 ArrayList 的例子

C# 早期的集合叫 ArrayList,它的「装东西」方法长这样(参数是 object):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections;

class Program
{
static void Main()
{
ArrayList list = new ArrayList();

// 往里塞的时候,自动装箱
list.Add(25.6); // double 装箱
list.Add(true); // bool 装箱
list.Add(13824); // int 装箱

// 往外取的时候,必须拆箱
double temp = (double)list[0];
bool on = (bool)list[1];
int raw = (int)list[2];

Console.WriteLine(temp); // 25.6
Console.WriteLine(on); // True
Console.WriteLine(raw); // 13824
}
}

因为 ArrayList 只能装 object,所以塞进去自动装箱,取出来手动拆箱。这就是拆箱装箱的由来。

不过现在写新代码,我们几乎不用 ArrayList 了,都用泛型集合(比如 List<int>),泛型不需要装箱拆箱。但你仍然会在老代码、某些 API、反射里碰到装箱拆箱,所以概念必须懂。


五、拆箱最大的坑:类型必须完全对上

这是新手必踩的坑,务必看仔细。拆箱时,盒子里的类型必须和你拆箱写的类型一模一样,差一点都不行。

1
2
3
object box = 100;         // 盒子里装的是 int

double d = (double)box; // 报错!int 不能直接拆成 double
为什么 int 不能直接拆成 double?

因为拆箱要求类型完全一致:盒子装的是 int,你就只能用 int 去拆,用 double 去拆会抛 InvalidCastException 异常(程序崩溃)。

那想把它变成 double 怎么办?分两步:先拆回 int,再让 int 隐式转成 double

1
2
3
4
5
object box = 100;
int temp = (int)box; // 第一步:先拆回 int(类型一致)
double d = temp; // 第二步:int 隐式转成 double

Console.WriteLine(d); // 100

记住口诀:拆箱要同型,先拆回原类型,再谈转换。

再看一个会翻车的例子:

1
2
object box = 3.14;        // 盒子里装的是 double
int x = (int)box; // 报错!double 不能拆成 int

类型一旦对不上,程序直接崩,所以拆箱前一定想清楚盒子里到底装的是啥。


六、性能提醒:别没事瞎装箱

装箱要在堆上开新空间、复制数据,是有成本的。如果在一个循环里反复装箱拆箱,程序会变慢。所以结论是——能不用就不用。

什么时候容易不小心装箱?

比如用 + 拼接字符串时,数值会先装箱再拼接。虽然单次看不出差别,但循环几十万次就有感觉了。新手阶段先记住这个意识即可,等学到泛型、集合再回头看会更明白。


七、上位机里的场景

上位机开发里,装箱拆箱常出现在「通用点位」这种需求上:一个变量要能同时存温度、开关、计数等不同类型,就会先用 object 来中转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Program
{
static void Main()
{
// 一个通用的点位值,先用 object 装,啥类型都能塞
object tag1 = 25.6; // 温度:double 装箱
object tag2 = true; // 开关:bool 装箱
object tag3 = 13824; // 原始值:int 装箱

// 用的时候再按各自类型拆出来
double temp = (double)tag1;
bool on = (bool)tag2;
int raw = (int)tag3;

Console.WriteLine("温度:" + temp);
Console.WriteLine("开关:" + on);
Console.WriteLine("原始值:" + raw);
}
}
结合之前学的模拟量换算

拆出来的原始值,还能接上之前学的换算:

1
2
3
4
5
object tagRaw = 13824;                 // int 装箱
int raw = (int)tagRaw; // 先拆回 int
double real = raw / 27648.0 * 100.0; // 再算实际温度

Console.WriteLine("实际温度:" + real + " °C"); // 实际温度:50 °C

八、小结

这一篇我们搞懂了拆箱装箱:

  • 值类型(int double bool)装的是值本身,引用类型(object string)装的是地址。
  • 装箱:值类型塞进 object,隐式自动完成。
  • 拆箱:从 object 取出值,必须写 (类型) 强制转换。
  • 最大的坑:拆箱要同型,盒子里是 int 就只能拆成 int,否则报错。
  • 能不用就不用,装箱拆箱有性能成本。

口诀:

装箱自动往里塞,拆箱同型往外拽

理解值类型装的是值本身,引用类型装的是地址

能写出 object box = 100; 这种装箱代码

知道拆箱必须写 (类型),而且类型要完全一致

知道 object box = 100; double d = (double)box; 会报错


九、能跑起来的小练习

点我看完整练习代码
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
using System;

class Program
{
static void Main()
{
// 1) 装箱:三个不同类型的值塞进 object
object a = 100; // int 装箱
object b = 3.14; // double 装箱
object c = true; // bool 装箱

// 2) 拆箱:按各自类型取出来
int x = (int)a;
double y = (double)b;
bool z = (bool)c;

Console.WriteLine(x); // 100
Console.WriteLine(y); // 3.14
Console.WriteLine(z); // True

// 3) 试试拆箱后隐式转成别的类型
double x2 = (int)a; // 先拆回 int,再隐式转 double
Console.WriteLine(x2); // 100
}
}