C# 类型系统:值类型、引用类型和装箱
梳理 C# 中值类型、引用类型、装箱与拆箱的核心区别,帮助写代码时更准确地判断复制、分配和性能成本。
C# 的类型系统看起来很亲切,但真正写出稳定代码时,最先要弄清楚的是:一个值到底是被复制,还是多个变量指向同一个对象。
值类型和引用类型
值类型通常直接保存数据本身,例如 int、double、bool、DateTime、enum 和 struct。当你把一个值类型变量赋给另一个变量时,复制的是值。
var a = 10;
var b = a;
b = 20;
Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
引用类型保存的是对象引用,例如 string、数组、class、接口类型和委托。把引用类型变量赋给另一个变量时,复制的是引用,两个变量会指向同一个对象。
var first = new List<int> { 1, 2 };
var second = first;
second.Add(3);
Console.WriteLine(first.Count); // 3
这不是“引用类型不会复制”,而是“复制的是引用”。理解这句话,很多看似奇怪的副作用就会变得很清楚。
struct 不等于一定高性能
struct 是值类型,常用于表示小而不可变的数据,例如坐标、金额、范围、时间片段。它的优势是语义明确,也可能减少堆分配。
但 struct 并不是万能性能按钮。过大的结构体会在传参、赋值、返回时产生复制成本。如果结构体还包含可变状态,代码更容易出现“改的是副本,不是原值”的误解。
一个实用判断是:结构体应该小、简单、不可变,并且代表一个值。
public readonly struct Money
{
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public decimal Amount { get; }
public string Currency { get; }
}
装箱和拆箱
装箱是把值类型包装成引用类型对象,常见于把值类型赋给 object 或非泛型集合。
int number = 42;
object boxed = number; // 装箱
int unboxed = (int)boxed; // 拆箱
装箱会产生额外分配,拆箱还需要类型检查。在普通业务代码里偶尔出现问题不大,但在高频循环、日志参数、旧式集合中反复装箱,就可能变成真实成本。
优先使用泛型集合可以避免很多无意义的装箱。
var numbers = new List<int>();
numbers.Add(42); // 不需要装箱
string 是引用类型,但像值一样使用
string 是引用类型,同时又是不可变对象。每次“修改”字符串,实际都是创建新的字符串。
var text = "hello";
text += " world";
这段代码不会修改原来的 "hello",而是生成一个新的字符串。如果在循环中频繁拼接,应该考虑 StringBuilder。
小结
理解值类型和引用类型,不只是为了背概念。它会影响 API 设计、并发安全、性能判断和 bug 定位。
日常写 C# 时,可以先问自己三个问题:
- 这个类型表达的是“一个值”,还是“一个对象”?
- 赋值或传参后,修改会不会影响原来的数据?
- 有没有无意中触发装箱、复制或共享可变状态?
这些问题看起来基础,却经常决定代码后面好不好维护。