C# 类型系统:值类型、引用类型和装箱

梳理 C# 中值类型、引用类型、装箱与拆箱的核心区别,帮助写代码时更准确地判断复制、分配和性能成本。

C# 的类型系统看起来很亲切,但真正写出稳定代码时,最先要弄清楚的是:一个值到底是被复制,还是多个变量指向同一个对象。

值类型和引用类型

值类型通常直接保存数据本身,例如 intdoubleboolDateTimeenumstruct。当你把一个值类型变量赋给另一个变量时,复制的是值。

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# 时,可以先问自己三个问题:

  • 这个类型表达的是“一个值”,还是“一个对象”?
  • 赋值或传参后,修改会不会影响原来的数据?
  • 有没有无意中触发装箱、复制或共享可变状态?

这些问题看起来基础,却经常决定代码后面好不好维护。