值类型与引用类型
本教程共 100 篇 · 第 9 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
9. 值类型与引用类型
本节目标:搞懂值类型和引用类型的根本区别,理解为什么有的赋值”各改各的”、有的”改一个全都变”。
这是 C# 里最容易被忽略、却又最影响直觉的一课。很多”怪现象”都源于此:两个变量明明分开赋值,改一个却把另一个也改了。
先有个通俗画面
程序运行时,数据主要存在两块内存:栈(stack)和堆(heap)。
- 栈像一排带编号的格子,存取极快,放”小而短命”的数据,用完立刻收回。
- 堆像一大片仓库,空间大、灵活,放”大或长寿”的数据,由垃圾回收(GC)事后清理。
不用死记硬背这两个词。你只要记住结论:值类型的数据直接放在变量”自己身上”,引用类型只存一个”地址”,真正数据在别处。
为什么 C# 要分两块内存?因为数据的”寿命”和”大小”差别太大。函数里的临时计数活几毫秒,用户的订单可能活整个会话。栈适合”快进快出”的小数据,由 CPU 直接管理、几乎零成本;堆适合”要一直留着、还可能很大”的对象,用 GC 统一回收,省去你手动释放的麻烦。把这两类分开,性能和安全都能兼顾。具体哪类数据进栈、哪类进堆,恰恰由它是值类型还是引用类型决定——这正是本节的核心。
值类型:复制的是”值本身”
int、double、bool、struct 等都是值类型(value type)。赋值时会把值整体复制一份:
namespace CSharpDemo;
int a = 10;
int b = a; // 把 10 这个值复制给 b
b = 20;
Console.WriteLine($"a={a}, b={b}"); // a=10, b=20
b = a 是把 a 里的 10 拷过去,之后 b 怎么改都和 a 无关。两个变量各装各的。
把它想成复印文件:你复印了一份合同给同事,他在自己那份上涂改,你的原件纹丝不动。值类型的”复制即独立”就是这种感觉。
引用类型:复制的是”地址”
string、数组、class 实例是引用类型(reference type)。变量里存的不是数据,而是”数据在哪”的地址。
namespace CSharpDemo;
class Person
{
public string Name { get; set; }
}
var p1 = new Person { Name = "小明" };
var p2 = p1; // 复制的是地址,两人指向同一对象
p2.Name = "小红";
Console.WriteLine(p1.Name); // 输出:小红
p2 = p1 没复制对象,只复制了”指向同一个对象的地址”。所以 p2.Name 一改,p1 看到的也是改后的——它们是同一个东西的两个”遥控器”。
遥控器这个比喻很关键:你家里电视只有一个,但遥控器可以有两支。按任何一支换台,电视都变。引用类型的多个变量就是”多支遥控器指向同一台电视”,谁都能操作那唯一的实体。
Note这就是新手最懵的地方:引用类型赋值后,两个变量共享一个对象。想完全独立,要”浅拷贝/深拷贝”,属于进阶话题,先记住这个行为即可。
struct 与 class 的直觉
- struct(结构):默认是值类型,适合”小、简单、像数值”的数据,如坐标、颜色、金额分量。
- class(类):默认是引用类型,适合”有行为、会被多处共享”的对象,如人、订单、账号。
一个对比:
namespace CSharpDemo;
struct Point
{
public int X;
public int Y;
}
var s1 = new Point { X = 1, Y = 2 };
var s2 = s1; // 值复制,互不影响
s2.X = 9;
Console.WriteLine(s1.X); // 仍是 1
把 struct 换成 class,s1.X 就会变成 9。这就是值/引用语义差异的直接体现。
Tip初学不必纠结何时用 struct。记住:绝大多数业务对象用
class;只有明确想要”赋值时复制、互不影响”的数值型小数据,才考虑struct。第 92 章会专门讲选型。
一个易混点:string 的特殊性
string 是引用类型,但行为像值类型——你改一个字符串变量,不会影响另一个:
string s1 = "hello";
string s2 = s1;
s2 = "world";
Console.WriteLine(s1); // 仍是 hello
原因有两个:一是字符串不可变(immutable),每次”修改”其实是生成新字符串;二是背后有字符串驻留优化。所以你写字符串时,可以当它值类型那么安心,但要知道它底层仍是引用。
注意这里和 Person 例子的区别:s2 = "world" 是把 s2 这支遥控器重新指向了一台新电视,并没有动 s1 指向的那台。换成对象属性 p2.Name = "小红",则是通过遥控器去改电视本身的内容——这两种”改”的本质不同,正是引用类型最容易让人绕晕的地方。
方法传参也遵循同一规则
把变量传给方法时,值类型传”副本”,引用类型传”地址”:
void ChangeInt(int x) => x = 999;
void ChangeName(Person p) => p.Name = "改了";
int n = 1;
ChangeInt(n);
Console.WriteLine(n); // 1,没变
var person = new Person { Name = "原" };
ChangeName(person);
Console.WriteLine(person.Name); // 改了
这就是为什么方法里改了引用类型的属性会”漏”出来,而改值类型参数不会。参数传递的细节,第 50 章还会展开。
Warning别误以为”引用类型方法里重新赋值参数”会影响外面。比如方法里写
p = new Person();,只是把局部的那支遥控器指向了新对象,外面的person仍然指向旧对象,不受影响。只有”通过参数去改对象的属性”才会漏出去。这个区别面试和工作里都常被问到。
为什么区分它很重要
因为这个差异直接决定”你的修改会波及哪里”。如果你以为两个变量独立,实际却共享一个对象,就会写出”改 A 影响 B”的隐蔽 bug,而且极难排查。
经典翻车现场:把 List<int> 赋给另一个变量后,往其中一个里加元素,另一个也变了——因为 List 是引用类型,两个变量指向同一个列表。想要独立副本,得显式复制(如 new List<int>(other))。
Tip写代码时多问一句:“这个变量存的是值,还是地址?“养成这个直觉,能避开一半以上的”莫名其妙”错误。
数组属于哪一类
数组是引用类型。所以 int[] a = [1,2,3]; int[] b = a; 之后,改 b[0] 会影响 a[0]。这点和单个 int 值类型相反,新手常在这里栽跟头。
int[] a = [1, 2, 3];
int[] b = a; // 共享同一数组
b[0] = 99;
Console.WriteLine(a[0]); // 99,被改了
记住:数组、字符串、类实例都是引用类型;int、double、bool、struct 是值类型。判断不清时,按这个清单对。
值类型一定更快吗
很多人听说”值类型在栈上、不用 GC”就觉得它一定更快,其实没那么简单:
- 小的值类型(如
int、Point)复制便宜,又避开了堆分配和 GC,确实常更快。 - 但如果是很大的
struct(几十个字段),每次赋值都要整体拷贝,成本可能反超引用类型的”只拷地址”。
所以别为了”快”盲目用 struct。先看语义——它像不像一个数值?像就用,性能是顺带的好处;不像就老老实实用 class。
Note值类型的”默认值”和引用类型也不同:未赋值的值类型字段会是
0/false这种零值;引用类型字段默认是null(“空遥控器”,什么也没指向)。访问null的属性会抛NullReferenceException,这是初学者第二大常见异常源。
如何拿到独立副本
如果确实需要两个变量互不影响,又偏偏是引用类型(比如一个 List),就要主动复制:
List<int> a = [1, 2, 3];
List<int> b = new List<int>(a); // 显式复制,得到新列表
b.Add(4);
Console.WriteLine(a.Count); // 3,原列表没被影响
这种”按现有内容造一份新的”就是浅拷贝。更复杂对象里还嵌着引用类型时,浅拷贝不够,需要深拷贝——那是进阶话题,先建立”引用类型共享、想独立得主动复制”的意识即可。
常见疑问解答
问:既然 struct 是值类型,那它比 class 快吗? 不一定。小数据用 struct 能避免堆分配、减轻 GC 压力,确实可能更快;但大 struct 复制成本高,反而更慢。别为了”快”盲目用 struct,先按语义(是否像数值)选,性能是次要考量。
问:string 是引用类型,为什么改了不影响别人? 因为字符串不可变:任何”修改”都生成新串,原串没动。再配合字符串驻留优化,让你用起来像值类型一样安心。但它底层仍是引用,别和值类型画等号。
问:方法里改了引用类型的属性,为什么会”漏”出来? 正因为传的是地址,方法内通过地址找到同一个对象并改了它的属性,外面自然看到变化。这是引用类型的本职行为。想隔离,就传副本或新建对象。
问:怎么快速判断一个类型是值类型还是引用类型?
简单记:基础数字类型、bool、char、struct、枚举是值类型;string、数组、class 实例是引用类型。更严谨地说,所有直接继承 System.ValueType 的都是值类型,其余引用类型继承 System.Object。
要点速记
- 值类型存”值本身”,赋值即复制、各改各的。
- 引用类型存”地址”,赋值共享同一对象。
- struct 默认值类型、class 默认引用类型。
- 数组、字符串、类实例都是引用类型。
- 引用类型默认值是
null,访问其成员当心空引用。
动手练一练
- 写两个
int变量a、b,让b = a后修改b,验证a不受影响(值类型)。 - 定义一个
Person类(含Name),p1赋值后p2 = p1,改p2.Name看p1.Name是否同步(引用类型)。 - 把上面的
class Person改成struct Person,重跑同一段代码,观察行为差异并解释原因。
小结与下一步
值类型存”值本身”,赋值即复制、各改各的;引用类型存”地址”,赋值共享同一对象。struct 默认值类型、class 默认引用类型。这个区分贯穿你之后的所有 C# 代码,也决定了”你的修改会波及哪里”。
下一章我们把数值类型拆开看:整型和浮点型到底有什么不同,金额为什么非得用 decimal。