Skip to content

05a-装箱拆箱(Boxing / Unboxing)

装箱(Boxing)和拆箱(Unboxing)是值类型与引用类型之间相互转换的过程。理解装箱拆箱对于编写高性能 C# 代码非常重要。


一、什么是装箱和拆箱

基本概念

操作方向说明
装箱(Boxing)值类型 → object 或接口类型在堆上创建对象副本,复制值数据
拆箱(Unboxing)object → 原始值类型从堆上对象提取值到栈变量

内存变化

csharp
int value = 42;

// 装箱前:值在栈上
// 栈:┌──────┐
//     │  42  │ ← value
//     └──────┘

object boxed = value;

// 装箱后:堆上创建副本
// 栈:┌──────────┐       堆:┌──────────────┐
//     │  0xF0A1  │──────→  │      42      │
//     └──────────┘         └──────────────┘
//      引用/地址              实际数据副本

// 拆箱:从堆上取值放回栈
int unboxed = (int)boxed;
// 栈:┌──────┐
//     │  42  │ ← unboxed(新副本)
//     └──────┘

代码示例

csharp
// 装箱:值类型 → object
int number = 123;
object boxed = number;  // 装箱:栈上的 123 复制到堆上
boxed = 456;            // 创建新的装箱对象,不影响 number

// 拆箱:object → 值类型(必须显式转换)
int unboxed = (int)boxed;   // 拆箱 + 复制
Console.WriteLine(unboxed);  // 456

// 装箱:值类型 → 接口
int number2 = 42;
IComparable comparable = number2;  // 装箱!int 实现 IComparable
int result = (int)comparable;      // 拆箱

// 拆箱类型必须精确匹配
object obj = 42;
// double d = (double)obj;  // ❌ InvalidCastException!
double d = (int)obj;        // ❌ 也不行,必须拆箱为原始类型
double d2 = (int)obj;       // ❌
double d3 = (double)(int)obj;  // ✅ 先拆为 int,再转 double

二、装箱拆箱的发生时机

1. 赋值给 object 类型

csharp
int val = 10;
object o = val;       // 装箱
object o2 = (object)val;  // 显式装箱(不必要,同上)

2. 赋值给接口类型

csharp
int val = 10;
IComparable c = val;  // 装箱(int 实现了 IComparable)

3. 使用非泛型集合

csharp
// ❌ 装箱:ArrayList 存 object
ArrayList list = new ArrayList();
list.Add(42);         // 装箱 × 1
list.Add(99);         // 装箱 × 2
int x = (int)list[0]; // 拆箱 × 1

// ✅ 无装箱:泛型 List<T>
List<int> list2 = new List<int>();
list2.Add(42);        // 无装箱
list2.Add(99);        // 无装箱
int y = list2[0];     // 无拆箱

4. 值类型调用基类方法(特殊)

csharp
int val = 42;
// 调用 object 的 ToString——int 重写了,一般不装箱
string s = val.ToString();  // 不装箱(int 重写了 ToString)

// 调用 object 的 GetType——会装箱
Type t = val.GetType();  // 装箱!因为 GetType 不是值类型方法

// 调用非重写的虚方法——会装箱
// 如果值类型没有重写某个虚方法,调用时需装箱

三、性能影响

为什么装箱拆箱慢

原因说明
内存分配装箱在堆上分配内存,需要 GC 管理
内存复制值数据从栈复制到堆(装箱),从堆复制到栈(拆箱)
GC 压力装箱对象增加 GC 负担
类型检查拆箱时需要运行时类型检查

基准测试对比

csharp
using System.Diagnostics;

// 测试:1000 万次操作
const int count = 10_000_000;

// 测试 1:无装箱(直接 int 操作)
Stopwatch sw = Stopwatch.StartNew();
int sum1 = 0;
for (int i = 0; i < count; i++)
    sum1 += i;
sw.Stop();
Console.WriteLine($"无装箱:{sw.ElapsedMilliseconds}ms");

// 测试 2:有装箱(ArrayList)
ArrayList list = new ArrayList();
sw.Restart();
for (int i = 0; i < 100_000; i++)  // 少两个数量级,否则太慢
    list.Add(i);
sw.Stop();
Console.WriteLine($"有装箱(10万次):{sw.ElapsedMilliseconds}ms");

// 测试 3:泛型(无装箱)
List<int> genericList = new List<int>();
sw.Restart();
for (int i = 0; i < 100_000; i++)
    genericList.Add(i);
sw.Stop();
Console.WriteLine($"泛型(10万次):{sw.ElapsedMilliseconds}ms");

// 输出示例(实际值因环境而异):
// 无装箱:45ms
// 有装箱(10万次):120ms
// 泛型(10万次):15ms
// ⚠️ 装箱版本比泛型版本慢 5-10 倍

四、装箱拆箱的三个常见陷阱

陷阱 1:多次装箱

csharp
interface IAnimal { void Speak(); }
struct Dog : IAnimal
{
    public void Speak() => Console.WriteLine("汪!");
}

Dog dog = new Dog();

// ✅ 正确:对象直接在栈上调用方法
dog.Speak();  // 无装箱

// ❌ 每次通过接口调用都会装箱
IAnimal animal = dog;  // 装箱(第一次)
animal.Speak();        // 使用已装箱对象(不再装箱)
// 但上面这行实际上是:
((IAnimal)dog).Speak();  // ❌ 再次装箱!每次强制转接口都装箱

陷阱 2:集合修改 struct 时意外装箱

csharp
struct Point { public int X; }

// 使用 ArrayList(object 集合)
ArrayList points = new ArrayList();
points.Add(new Point { X = 1 });  // 装箱

// ❌ 不能这样修改(编译错误)
// points[0] 是 object,不是 Point
// ((Point)points[0]).X = 10;  // 拆箱 → 修改的是临时副本,不影响集合

// ✅ 正确做法
Point p = (Point)points[0];  // 拆箱
p.X = 10;
points[0] = p;  // 再次装箱

陷阱 3:可空值类型的装箱

csharp
int? nullable = 42;
object boxed = nullable;        // 装箱:如果 HasValue=true,装箱内部值
Console.WriteLine(boxed.GetType());  // System.Int32(不是 Nullable<Int32>)

int? nullNullable = null;
object boxedNull = nullNullable;  // 装箱结果为 null(不是装箱对象)
Console.WriteLine(boxedNull == null);  // True

// 拆箱可空类型
int? unboxed = (int?)boxed;     // ✅ 可以
int? unboxed2 = (int?)42;       // ✅ 也可以
int? unboxed3 = boxed as int?;  // ✅ 推荐:安全转换

五、如何避免不必要的装箱拆箱

1. 使用泛型集合

csharp
// ❌ 装箱
ArrayList list = new ArrayList { 1, 2, 3 };

// ✅ 无装箱
List<int> list = new List<int> { 1, 2, 3 };

2. 使用泛型方法而非 object 参数

csharp
// ❌ 装箱
void PrintObject(object obj) => Console.WriteLine(obj);

// ✅ 无装箱
void PrintValue<T>(T value) where T : struct
{
    Console.WriteLine(value);  // 值类型调用时无装箱
}

PrintValue(42);  // 无装箱

3. 值类型重写 ToString 等基类方法

csharp
struct Point
{
    public int X; public int Y;

    // ✅ 重写 ToString,调用时不装箱
    public override string ToString() => $"({X}, {Y})";

    // ❌ 如果不重写,直接调用 ToString 会装箱
    // 因为要调用 object 的虚方法
}

var p = new Point { X = 1, Y = 2 };
Console.WriteLine(p);  // 如果 Point 重写了 ToString,不装箱

// 调用 GetType 总是会装箱
// Type t = p.GetType();  // 装箱!GetType 不能被重写

4. struct 避免实现接口(除非必要)

csharp
// ❌ struct 实现接口后,接口赋值会装箱
struct MyStruct : IComparable { /* ... */ }

// ✅ 如果不需要多态,不要实现接口
struct MyPureStruct { /* ... */ }

// 如果需要比较,实现 IComparable<T>(泛型版本)
struct MyStruct : IComparable<MyStruct>
{
    public int Value;
    public int CompareTo(MyStruct other) => Value.CompareTo(other.Value);
}

5. 使用 ref 传递 struct 避免复制

csharp
struct LargeStruct { /* 较大 */ }

// ❌ 按值传递:复制整个结构
void Process(LargeStruct data) { }

// ✅ 按引用传递:不复制,无装箱
void Process(ref LargeStruct data) { }

// ✅ 只读引用
void Process(in LargeStruct data) { }

六、检测装箱拆箱

使用反编译工具查看 IL 代码

装箱操作在 IL 中对应 box 指令,拆箱对应 unboxunbox.any 指令。

csharp
int val = 42;
object obj = val;  // IL: box [mscorlib]System.Int32
int val2 = (int)obj;  // IL: unbox.any [mscorlib]System.Int32

常见的装箱指令触发点

代码模式是否装箱
object o = valueType;
interface i = valueType;
list.Add(valueType)(ArrayList)
list.Add(valueType)(List<T>)
valueType.ToString()(已重写)
valueType.GetType()
valueType.GetHashCode()(已重写)
Enum.HasFlag()是(Enum 是值类型)

核心知识点总结

装箱拆箱对照

操作方向复制方式性能
装箱值类型 → 引用类型栈 → 堆(完整复制)慢(分配+复制)
拆箱引用类型 → 值类型堆 → 栈(完整复制)较快(检查+复制)

注意事项

  1. 装箱在堆上分配内存——频繁装箱增加 GC 压力
  2. 拆箱类型必须精确匹配——(int)obj 要求 obj 原本就是 int 装箱而来
  3. 优先使用泛型集合——List<T> 替代 ArrayListDictionary<K,V> 替代 Hashtable
  4. 值类型重写 ToString 避免装箱——自定义 struct 始终重写 ToString()Equals()GetHashCode()
  5. 避免 struct 实现接口——接口赋值会装箱,若必须实现,使用泛型接口版本
  6. 装箱 5-10 倍慢于直接操作——大循环中避免装箱
  7. GetType() 总会装箱——因为它是 object 的方法,值类型不能重写
  8. 可空类型装箱有特殊规则——null 值装箱为 null,非 null 值装箱内部值

Released under the MIT License.