在 C# 中,装箱(Boxing) 和 拆箱(Unboxing) 是值类型与引用类型之间转换的特殊机制,仅发生在 值类型(Value Type) 和 引用类型(Reference Type) 相互转换的场景中。
一、值类型与引用类型的存储差异
- 值类型:包括
int
、float
、bool
、struct
、enum
等,直接存储数据本身,通常分配在栈(Stack) 上(特殊情况如struct
作为类的字段时会存放在堆上)。 - 引用类型:包括
object
、string
、class
、interface
等,存储数据的引用(地址),实际数据分配在堆(Heap) 上,栈上只保留指向堆的地址。
二、装箱(Boxing):值类型 → 引用类型
装箱是指将值类型转换为引用类型(通常是 object
或接口类型)的过程。
它的本质是:在堆上创建一个新的引用类型对象,将值类型的数据复制到这个对象中,并返回该对象的引用。
装箱的特点:
- 装箱是隐式转换(无需显式强制转换)。
- 会导致堆内存分配和数据复制,可能影响性能(尤其是频繁操作时)。
- 装箱后,堆上的对象与原始值类型变量是独立的(修改一方不影响另一方)
int i = 10;
object obj = i; // 装箱
i = 20; // 修改原始值类型
Console.WriteLine(obj); // 输出 10(堆上的副本未变)
注意一:值类型转换为任何引用类型(包括 object
和接口类型)时,都会触发装箱,并非仅转换为 object
时才会装箱。
注意二:在 C# 中,值类型(如 struct、int 等)通常只能隐式转换为 object
或其实现的接口类型,这两种转换会触发装箱;而值类型转换为其他引用类型(如自定义 class)几乎都是显式转换,且这类转换本质上是 “创建新的引用类型实例”,而非装箱。
示例:值类型转换为接口类型会触发装箱
// 定义一个接口(引用类型)
public interface IMyInterface { void Print(); }
// 定义一个值类型(struct)并实现接口
public struct MyStruct : IMyInterface
{
public void Print() => Console.WriteLine("MyStruct");
}
// 测试代码
MyStruct s = new MyStruct();
// 情况1:值类型转换为接口类型 → 触发装箱
IMyInterface iface = s; // 装箱:MyStruct(值类型)→ IMyInterface(引用类型)
// 情况2:值类型转换为 object → 触发装箱
object obj = s; // 装箱:MyStruct(值类型)→ object(引用类型)
三、拆箱(Unboxing):引用类型 → 值类型
拆箱是指将已装箱的引用类型(即通过装箱得到的 object
或接口)转换回原始值类型的过程。
它的本质是:验证引用类型对象的类型是否与目标值类型匹配,然后将堆上对象中的数据复制回栈上的值类型变量。
拆箱的特点:
- 拆箱是显式转换(必须使用强制转换符)。
- 拆箱前必须先完成装箱(不能对未装箱的引用类型拆箱)。
- 同样涉及数据复制,但不分配新的堆内存(仅验证和复制数据)。
- 类型必须严格匹配(例如,不能将装箱的
int
直接拆箱为long
)
object obj = 10; // 装箱 int
long l = (long)obj; // 报错:InvalidCastException(必须先拆为 int,再转 long)
long l = (long)(int)obj; // 正确:先拆箱为 int,再转换为 long
注意:
不属于装箱的案例:
1. 引用类型之间的转换
// string 是引用类型,转换为 object(基类引用类型)
string str = "hello";
object obj = str; // 不属于装箱!
// 原因:string 本身是引用类型,转换为 object 只是引用类型之间的向上转型,
// 仅传递引用地址,无需在堆上创建“值类型包装对象”,因此不是装箱。
在 C# 中,int
转 string
不会发生装箱,原因如下:
int
转 string
通常通过 int.ToString()
方法(或隐式调用该方法的场景,如字符串拼接、插值)实现,其过程是:直接根据 int
的数值生成一个新的 string
对象(字符序列),存储在堆上。
2. 引用类型转换值类型
int i = 10;
string str = i.ToString();
// 调用 int 自身的方法,无装箱(返回 string 是类型转换,非装箱)
不属于拆箱的案例:
1. 引用类型(非装箱对象)转换为值类型
// 示例:string(引用类型)转换为 int(值类型)
string str = "123";
int num = int.Parse(str); // 不是拆箱!
//str 是 string 类型的引用对象,并非通过 int 装箱得到。
2. 引用类型之间的转换
object obj = "hello"; // obj 指向 string 引用类型(非装箱对象)
string str = (string)obj; // 不是拆箱!
// 原因:
// 转换的双方都是引用类型(object 是基类,string 是派生类)
//本质是引用地址的转换,与值类型无关,因此不是拆箱。
3. 对未装箱的 object
强行转换为值类型(错误,但仍不属于拆箱)
object obj = new object(); // 普通 object 实例(未装箱)
int i = (int)obj; // 抛出 InvalidCastException,但这不是拆箱!
// 原因:
// obj 内部没有存储任何值类型数据(不是装箱产生的),
// 因此这个转换本质是“无效的类型转换”,而非拆箱。