首页 / C# 入门教程 / 装箱与拆箱

C# 入门教程

装箱与拆箱

本教程共 100 篇 · 第 48 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程值类型装箱拆箱

48. 装箱与拆箱

本节目标:理解值类型转引用类型的开销,看清 object 装箱的代价与替代方案。

什么是装箱与拆箱

值类型(如 int)存在栈上,引用类型存在堆上。object 是引用类型。当你把 int 塞进 object,运行时会把值复制到堆上、包成一个对象,这叫装箱(boxing)。反向取出来叫拆箱(unboxing)。

namespace CSharpDemo;

int number = 42;
object boxed = number;       // 装箱:值被复制到堆
int unboxed = (int)boxed;    // 拆箱:从堆取回值
Console.WriteLine(unboxed);

所有类型都源自 object(第 47 章),所以值类型也能赋值给 object 变量——代价就是这次装箱。理解它,是写出高性能 C# 的基础。

值类型在内存里的样子

值类型变量直接”装着”数据本身。赋值 int a = 1; int b = a; 会复制一份,ab 互不影响。而装箱把这份数据”拎”到堆上,再让 object 变量指向它。一次装箱 = 一次堆分配 + 一次内存拷贝。

这也解释了为什么装箱后有”两份数据”:原栈上的副本和堆上的盒子。改其一,另一不变。

装箱的代价在哪

装箱不是”免费”的。它要分配堆内存、拷贝数据,之后还要等垃圾回收(GC)清理。在循环里大量装箱,会让性能明显下滑,也会给 GC 增加压力。

更隐蔽的坑是:装箱后改原值,盒里的副本不变。

namespace CSharpDemo;

int x = 1;
object o = x;
x = 2;
Console.WriteLine(o); // 仍是 1,盒中副本未变
Note

拆箱必须显式转换,且类型必须精确匹配,否则会抛 InvalidCastException。把装箱的 int 强转成 long 都会失败。

Warning

在性能敏感的热路径(如游戏每帧、接口每请求)里反复装箱,积少成多会拖垮吞吐。这种开销单看一次微不足道,放进百万次循环就暴露了。

什么时候会隐式装箱

不只是手动赋值。把值类型当 object 参数传入、放进非泛型集合、或拼接字符串时,都可能触发装箱。下面这行就暗藏一次装箱:

namespace CSharpDemo;

object o = 3.14; // double 被装箱
Console.WriteLine("值:" + o); // 又借 ToString 间接使用

很多”看起来无害”的写法,背后都有装箱。性能敏感处要留意隐式转换。

还有一类常见陷阱:params object[] 的方法,传入值类型时会整体装箱成对象数组。

namespace CSharpDemo;

void Log(params object[] items) => Console.WriteLine(string.Join(",", items));

Log(1, 2, 3); // 三个 int 都被装箱进 object[]

ArrayList 的反面教材

早期 ArrayList 存的是 object,往里加 int 就会逐个装箱。既慢又丢失了类型安全。

namespace CSharpDemo;

System.Collections.ArrayList list = new();
list.Add(1);   // 每次 Add 都装箱
list.Add(2);
int first = (int)list[0]; // 拆箱

如今应使用泛型 List<int>,它在编译期锁定类型,完全避免装箱。

namespace CSharpDemo;

List<int> numbers = [1, 2, 3]; // 不装箱,类型安全
foreach (var n in numbers)
    Console.WriteLine(n);
Tip

经验法则:能用泛型集合(List<T>Dictionary<TKey,TValue>)就别用 ArrayListHashtable。它们是为装箱时代留下的旧工具。

泛型如何彻底避开装箱

泛型(List<T>Dictionary<K,V>)在编译期为每个具体类型生成专用版本。对 List<int>,内部直接存 int 数组,没有 object 中间层,自然不装箱。这是”泛型 vs 非泛型集合”性能差距的根本来源。

甚至方法也能泛型化:写过 T Max<T>(T a, T b) 后,值类型出入都不装箱。

还有 Span 这条捷径

若是处理一段内存里的数据,可用 Span<T> / ReadOnlySpan<T>。它们是栈上结构体,零拷贝地”看”向现有内存,既不涉及装箱,也不额外分配。在解析字符串、处理缓冲区时尤为高效。

装箱、拆箱与泛型的对照

场景是否装箱建议
object x = 1;避免,改用具体类型
List<int> = [1];首选泛型集合
Log(1, 2)params object[]高频处改泛型重载
Span<int> 看数组内存操作首选

怎么发现隐藏的装箱

肉眼难辨的装箱,靠工具更靠谱。在 Visual Studio 里开启性能探查或留意编译器对 object/ArrayList 的诊断提示,往往能揪出藏得很深的装箱点。日常写代码时也有个简单心法:凡看到 object 类型、非泛型集合、params object[],就停下来问一句”这里是不是绕开了具体类型”。多数情况下,换用泛型或具体类型就能让它消失。

Tip

一个零成本的实用习惯:把热点路径里的 ArrayListHashtable 全部替换成 List<T>Dictionary<K,V>。这一改既消装箱又补回类型安全,几乎不费力气,是性价比最高的优化之一。

Warning

别走向另一个极端:为”避免装箱”而滥用 struct。struct 越大,赋值时的整块拷贝成本越高;该用引用类型的地方强行用 struct,可能比一次装箱还慢。权衡”数据大小”与”拷贝开销”,才是正解,不要见装箱就躲。

新手常见坑

第一,误以为 object 万能就大量用。能不装箱就别装箱,泛型是更优解。

第二,拆箱类型写错。把装箱的 int 强转成 long 会直接抛异常,类型必须一致。

第三,在热路径(如每帧、每请求)里反复装箱,积少成多拖垮性能。用 Span<T> 或泛型避开。

第四,认为”struct 一定比 class 快”。struct 避免装箱的同时也可能因复制变大对象,需权衡,并非处处更快。

可空值类型与装箱

int? 这类可空值类型(nullable value type)同样会装箱。规则有点贴心:为 null 时装箱结果是 null,否则才把里面的 int 装箱。

namespace CSharpDemo;

int? maybe = null;
object boxed = maybe;          // 装箱为 null
Console.WriteLine(boxed == null); // True

maybe = 5;
boxed = maybe;                 // 这次才真正装箱 int
Console.WriteLine((int)(int?)boxed); // 拆箱回 int? 再转 int:5

boxed 拆箱回 int? 时,null 也能安全还原,比裸 int 更稳。但装箱动作本身依旧发生,性能账照算。

记住一个判断口诀:看见 object、看见非泛型集合、看见 params object[],就要警觉背后可能在装箱。养成”能用具体类型/泛型就别用 object”的习惯,性能与类型安全双收。装箱本身不可怕,可怕的是在热路径里不知不觉地反复装箱,单看一次微不足道,放进循环就放大成瓶颈。

常见疑问解答

问:值类型一定能避免装箱吗? 不。值类型赋值给 object、传给 object 参数、进非泛型集合时仍会装箱。只有留在具体类型或泛型里才不装箱。

问:可空值类型 int? 装箱后是什么? int? 装箱时,若为 null 得到 null,否则把里面的 int 装箱。拆箱回 int? 时,null 也能还原,这点比裸 int 安全。

问:拆箱失败会怎样? 拆箱要求类型精确匹配,否则抛 InvalidCastException。把装箱的 int 强转 long 会直接报错,类型必须一致。

问:为什么 ArrayList 是反面教材? 它存 object,加 int 每次都装箱,既慢又丢类型安全。现代代码一律用 List<T> 替代。

问:Span 真能完全替代装箱吗? 在”看一段已有内存”的场景下可以零拷贝零装箱;但若必须存进 object 或跨方法以 object 传递,仍躲不开装箱。它是性能利器,不是万能钥匙。

小结

装箱拆箱是”值类型与引用类型鸿沟”的代价:一次堆分配、一次拷贝、一次 GC。第 49 章的 record 则提供了一种更现代的值语义方案,许多场景能顺带避开装箱烦恼。