装箱是值类型隐式转为object或接口类型并分配堆内存,拆箱是显式转回且需类型检查;常见装箱场景包括Console.WriteLine(i)、list.Add(i)(List)、string.Format;泛型可彻底避免装箱。

装箱和拆箱在 C# 里到底发生了什么
装箱(boxing)是把 int、bool、struct 这类值类型隐式转成 object 或接口类型的过程;拆箱(unboxing)则是反向操作——从 object 显式转回原值类型。它们不是简单的类型标注变化,而是实实在在的内存分配和复制行为。
比如 int i = 42; object o = i; 这行就触发了装箱:CLR 在堆上为 o 分配新对象,把 i 的值拷贝过去;而 int j = (int)o; 拆箱时,CLR 要检查 o 是否确实装的是 int,再从堆中把值复制回栈。
- 装箱必然涉及堆内存分配,可能触发 GC
- 拆箱失败会抛出
InvalidCastException - 即使值类型实现了接口,装箱后调用接口方法仍会再发生一次装箱(如
IComparable.CompareTo)
哪些写法会悄悄触发装箱
很多看似无害的代码,背后都在高频装箱,尤其在泛型不普及的老代码或误用场景里:
-
Console.WriteLine(i)——WriteLine(object)重载被选中,i被装箱 -
list.Add(i),其中list是List或ArrayList -
string.Format("{0}", i)—— 参数数组是object[],每个值类型参数都装箱 - 对值类型调用非虚的接口方法,如
((IFormattable)i).ToString("D")
注意:int.ToString() 不装箱,但 ((object)i).ToString() 会先装箱再调用——哪怕你只是想“转成 object 再 ToString”。
用泛型替代 object 容器是最直接的解法
泛型在编译期生成专用代码,绕过装箱拆箱。这是现代 C# 避免该问题的首要手段:
- 用
List替代ArrayList或List - 用
Dictionary替代Hashtable - 自定义泛型方法处理值类型集合:
void Process,而非(IEnumerable items) void Process(IEnumerable items)
对于日志、调试等必须接受任意类型的场景,可优先用 string 或 Span 接收格式化结果,而不是依赖 object 参数。例如:Log($"Value: {i}") 比 Log(i) 更安全。
结构体设计时注意接口实现方式
如果自定义 struct 实现了接口(如 IComparable),调用接口成员时仍可能触发装箱——除非你同时提供对应的泛型方法或显式实现。
- 避免让结构体只实现接口而不提供对应实例方法(如只有
int IComparable.CompareTo(object other),没有int CompareTo(int other)) - 接口方法尽量用泛型约束,例如
int CompareTo(T other)而非int CompareTo(object other) - 使用
ref struct(如Span、ReadOnlySpan)可彻底排除装箱可能,但受限于栈生命周期
真正难优化的点往往不在单次装箱,而在循环体内反复装箱拆箱——比如在 for 循环里对 int 做 ToString() 后又塞进 ArrayList。这种地方改起来要动数据流,不是加个 就能解决的。











