装箱 = 值类型 → 引用类型(堆上分配)
拆箱 = 引用类型 → 值类型(堆上取数据)
它们是隐式类型转换,底层由 CLR 自动完成。
1. 先搞懂两个基础:栈 和 堆
C# 内存分两块:
- 栈(Stack) :存值类型(int、double、struct、bool),轻量、自动释放、速度极快。
- 堆(Heap) :存引用类型(string、object、类实例),需要 GC 管理、速度慢。
值类型本身只在栈上,根本不在堆里。
2. 装箱(Boxing)底层到底做了什么?
cs
int a = 10; // 栈上的值类型
object obj = a; // 这里发生了 **装箱**
底层 3 步动作(CLR 自动执行)
-
在托管堆上分配一块新内存 大小 = 值类型本身大小 + 对象头(Type Handle + Sync Block)
-
把栈上的值类型 复制 到堆上 栈上的
a = 10原样拷贝到堆内存里。 -
返回堆上对象的地址 赋值给
obj引用变量。
内存变化图
cs
【栈】 【堆】
a = 10 → 分配新对象
拷贝 10 进去
obj = 堆地址 ← 返回堆地址
一句话
装箱 = 堆上新建对象 + 栈数据拷贝到堆
3. 拆箱(Unboxing)底层做了什么?
cs
object obj = 10; // 先装箱
int b = (int)obj; // 这里发生 **拆箱**
底层 3 步动作
-
CLR 检查堆上对象 确认它确实是被装箱的 int ,不是别的类型,否则直接抛
InvalidCastException。 -
获取堆中值类型数据的地址 找到堆里那个真正的
10。 -
把数据 复制回栈 拷贝到栈上的变量
b里。
内存变化
cs
【堆】 【栈】
装箱的 int → 取出值 10
复制到 b
b = 10
一句话
拆箱 = 类型检查 + 堆数据拷贝回栈
4. 性能代价(为什么要少用装箱?)
装箱的成本(非常高)
-
堆内存分配
-
数据拷贝
-
GC 压力(堆对象多了,GC 就要干活)
拆箱的成本
-
类型检查
-
数据拷贝
对比
-
直接赋值:1 个 CPU 指令
-
装箱:几十~上百个 CPU 指令
频繁装箱 = 程序明显变慢。
5. 最容易触发装箱的 3 种代码
1)把值类型扔进 object / 非泛型集合
cs
int a = 10;
object b = a; // 装箱
ArrayList list = new ArrayList();
list.Add(10); // 装箱!
2)值类型调用 ToString () / GetHashCode () 等重写方法
cs
int a = 10;
string s = a.ToString(); // **不装箱**(因为重写了)
int a = 10;
a.GetType(); // 装箱!(没重写,会转成 object)
3)字符串拼接 + 值类型
cs
int a = 10;
string s = "年龄:" + a; // 装箱!
C# 会把 a 转成 object 再拼接。
6. 如何避免装箱?
- 用泛型集合 (
List<int>而不是ArrayList) - 避免值类型转 object
- 字符串拼接用
$""或string.Format不会装箱(C# 编译器优化) - 用重载方法,不要传 object
装箱
-
栈 → 堆
-
分配堆内存
-
拷贝值类型数据
-
返回引用
-
性能开销大
拆箱
-
堆 → 栈
-
类型检查
-
拷贝数据回栈
-
必须类型完全匹配
本质
装箱拆箱 = 内存拷贝 + 栈堆转换
总结
- 装箱 :值类型 → 引用类型,堆分配 + 数据拷贝
- 拆箱 :引用类型 → 值类型,类型检查 + 数据拷贝回栈
- 成本:装箱 > 拆箱 > 普通赋值
- 优化:用泛型、避免转 object、避免频繁字符串拼接