C#的类型系统把所有类型分成两大阵营:值类型和引用类型。这个划分不是语法层面的装饰,而是直接决定了数据在内存中如何存储、赋值时是复制数据还是复制地址、参数传递时方法内部能否修改外部变量。理解这一点,是看懂C#程序行为的第一步。本文从内存原理、代码表现、装箱拆箱三个层面,把这两类类型的区别讲清楚。

一、内存分配:栈与堆的本质区别
值类型包括int、double、bool、char、struct以及枚举等,它们的实例通常直接存储在栈上(或作为对象的一部分内联存储在堆中)。所谓直接存储,指的是变量本身就包含了完整的数据。比如声明一个int age = 25;,栈上这个位置存放的就是25这个数值本身,没有任何中间环节。
引用类型包括class、string、array、delegate、interface等。声明一个引用类型变量时,栈上存放的只是一个地址(引用),真正的对象数据在托管堆上。变量就像一张写着门牌号的纸条,顺着地址找过去,才能看到实际的房子。
这种存储差异带来一个直接后果:值类型变量的读写不需要间接寻址,访问速度快;引用类型每次访问都要先取出地址再跳转到堆上的实际位置。但要注意,值类型赋值时会整体复制数据,如果结构体很大,复制开销反而可能超过引用的间接开销,所以并非值类型永远更快。
二、赋值行为与代码演示
两类类型最直观的区别体现在赋值操作上。值类型赋值是深拷贝,两个变量从此互不相干;引用类型赋值只是复制了地址,两个变量指向同一个对象,改一个另一个也变。看下面这段代码:
struct PointStruct
{
public int X;
public int Y;
}
class PointClass
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
// 值类型赋值:复制完整数据
PointStruct s1 = new PointStruct { X = 1, Y = 2 };
PointStruct s2 = s1;
s2.X = 100;
Console.WriteLine(s1.X); // 输出 1,s1 不受影响
// 引用类型赋值:只复制引用
PointClass c1 = new PointClass { X = 1, Y = 2 };
PointClass c2 = c1;
c2.X = 100;
Console.WriteLine(c1.X); // 输出 100,c1 和 c2 指向同一对象
}
}参数传递也遵循同样的逻辑。默认情况下(不加ref),方法收到的是变量的副本。对值类型参数的修改不会影响调用方;对引用类型参数的修改虽然会改变对象内容(因为副本里存的地址指向原对象),但如果在方法内把参数指向一个新对象,调用方依然感知不到。如果想真正按引用传递,需要显式使用ref或out关键字。
还有一个容易被忽略的细节:string虽然是引用类型,却表现出值类型的不可变行为。对字符串做拼接、替换等操作,返回的都是新对象,原字符串不会改变。这是.NET有意设计的,理解它能避免不少困惑。
三、装箱与拆箱:类型系统的交叉点
当值类型需要被当作引用类型对待时,比如把int放进ArrayList或赋值给object变量,就会发生装箱。装箱时CLR在堆上创建一个新对象,把值类型的值复制进去;拆箱则是反向操作,从堆对象中把值取回来。看示例:
int number = 42;
object boxed = number; // 装箱:在堆上创建对象并复制值
int unboxed = (int)boxed; // 拆箱:显式转换并复制回来
// 循环中反复装箱,性能损失明显
ArrayList list = new ArrayList();
for (int i = 0; i < 1000000; i++)
{
list.Add(i); // 每次Add都会装箱一次
}
// 使用泛型集合避免装箱
List<int> genericList = new List<int>();
for (int i = 0; i < 1000000; i++)
{
genericList.Add(i); // 不发生装箱
}装箱的代价不小:堆分配带来内存压力,频繁装箱会制造大量小对象,增加垃圾回收器的工作量;拆箱还有类型检查开销,且如果拆箱的目标类型与装箱时不一致,会直接抛出InvalidCastException。这就是泛型集合List<T>全面取代ArrayList的原因,泛型让值类型在集合中保持原样存储,零装箱成本。
四、如何选择:struct还是class
实际开发中选值类型还是引用类型,可以参考几条经验法则。微软官方建议:类型实例较小(约16字节以下)、生命周期短、不可变、且经常按值复制时,用struct比较合适,典型的例子是DateTime、坐标点、颜色值。反之,类型体积大、需要被多方共享修改、作为继承体系的一部分,就应该用class。
还要留意默认值的差异:值类型永远有默认值,int默认是0,结构体各字段取各自默认值,不会为null(可空值类型除外);引用类型默认是null,使用前必须检查,否则会撞上NullReferenceException。在可空引用类型特性开启后,编译器会帮助检查潜在的空引用问题,建议在新项目中启用。
总结一下:值类型存储数据本身,赋值即复制,分配在栈上或内联,不增加GC压力;引用类型存储地址,赋值共享对象,数据在托管堆上,由GC管理生命周期。掌握这条主线,再遇到变量行为诡异的问题时,从内存模型出发分析,基本都能找到答案。
C#值类型C#引用类型值类型与引用类型区别修改时间:2026-09-15 03:58:27