值类型赋值是深拷贝,修改副本不影响原变量;引用类型赋值是浅拷贝,仅复制堆地址,修改共享对象会影响所有引用者。

值类型和引用类型不是“语法风格差异”,而是内存行为、复制逻辑、生命周期管理的根本分叉——选错类型,轻则语义混乱,重则性能崩盘或 GC 压力陡增。
赋值时到底发生了什么
值类型赋值是深拷贝:栈上新开一块内存,把整个值(比如 int 的 4 字节)原样复制过去。改副本,原变量毫发无损。
引用类型赋值是浅拷贝:只复制堆地址(通常 4 或 8 字节指针),两个变量指向同一块堆内存。改其中一者的属性,另一者立刻可见变化。
-
int a = 10; int b = a; b = 20;→a还是 10 -
Person p1 = new Person(); Person p2 = p1; p2.Name = "Alice";→p1.Name也变成 "Alice" - 例外:
string是引用类型,但赋值后重新赋值(s2 = "World")会创建新对象,旧引用断开——这是不可变性带来的假象,不是值类型行为
装箱和拆箱必须显式对齐类型
把值类型往 object 或接口类型里塞,会触发装箱:在堆上分配新对象,复制值类型数据。这个过程隐式发生,但代价不低。
拆箱必须显式且严格匹配原始类型。常见错误:
-
int i = 42; object o = i; long j = (long)o;→ 运行时报InvalidCastException - 正确写法只能是
int j = (int)o;或int? j = (int?)o; - 循环中频繁对
int装箱(如往ArrayList添加)会显著拖慢性能,并推高 GC 频率
struct 里嵌套 class 字段是个隐蔽陷阱
结构体(struct)是值类型,但它的字段可以是引用类型。这时复制结构体,只复制字段的引用,不复制它指向的对象。
结果就是:两个结构体实例共享同一个嵌套对象。修改其中一个的引用字段内容,另一个也会变。
- 例如
TaggedInteger结构体含List<string></string>字段,n2.AddTag("B")后,n1的 tags 列表也多了 "B" - 这不是 bug,是设计使然;但若没意识到,极易写出非预期的共享逻辑
- 解决思路:要么用不可变集合(如
IReadOnlyList+ 每次新建),要么干脆改用class
判断类型不能只看 var 或 new 表达式
var x = 5; 推出 int,是值类型;var y = new StringBuilder(); 推出 StringBuilder,是引用类型——但这只是表象。
真正决定类型的,是定义源头:
- 所有继承自
System.ValueType的都是值类型(包括int、DateTime、自定义struct) - 所有直接继承自
System.Object且不继承ValueType的,是引用类型(class、string、Array、delegate) -
typeof(T).IsValueType是运行时最可靠的判断方式
最容易被忽略的点:值类型在作为类字段时,会随类一起分配在堆上;局部变量才大概率落在栈上——所以“栈 vs 堆”不能简单等同于“值 vs 引用”。










