首页 / C# 入门教程 / 值类型与引用类型

C# 入门教程

值类型与引用类型

本教程共 100 篇 · 第 9 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

C#C# 入门教程值类型引用类型structclass

9. 值类型与引用类型

本节目标:搞懂值类型和引用类型的根本区别,理解为什么有的赋值”各改各的”、有的”改一个全都变”。

这是 C# 里最容易被忽略、却又最影响直觉的一课。很多”怪现象”都源于此:两个变量明明分开赋值,改一个却把另一个也改了。

先有个通俗画面

程序运行时,数据主要存在两块内存:(stack)和(heap)。

  • 像一排带编号的格子,存取极快,放”小而短命”的数据,用完立刻收回。
  • 像一大片仓库,空间大、灵活,放”大或长寿”的数据,由垃圾回收(GC)事后清理。

不用死记硬背这两个词。你只要记住结论:值类型的数据直接放在变量”自己身上”,引用类型只存一个”地址”,真正数据在别处。

为什么 C# 要分两块内存?因为数据的”寿命”和”大小”差别太大。函数里的临时计数活几毫秒,用户的订单可能活整个会话。栈适合”快进快出”的小数据,由 CPU 直接管理、几乎零成本;堆适合”要一直留着、还可能很大”的对象,用 GC 统一回收,省去你手动释放的麻烦。把这两类分开,性能和安全都能兼顾。具体哪类数据进栈、哪类进堆,恰恰由它是值类型还是引用类型决定——这正是本节的核心。

值类型:复制的是”值本身”

intdoubleboolstruct 等都是值类型(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 换成 classs1.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,被改了

记住:数组、字符串、类实例都是引用类型;intdoubleboolstruct 是值类型。判断不清时,按这个清单对。

值类型一定更快吗

很多人听说”值类型在栈上、不用 GC”就觉得它一定更快,其实没那么简单:

  • 小的值类型(如 intPoint)复制便宜,又避开了堆分配和 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 是引用类型,为什么改了不影响别人? 因为字符串不可变:任何”修改”都生成新串,原串没动。再配合字符串驻留优化,让你用起来像值类型一样安心。但它底层仍是引用,别和值类型画等号。

问:方法里改了引用类型的属性,为什么会”漏”出来? 正因为传的是地址,方法内通过地址找到同一个对象并改了它的属性,外面自然看到变化。这是引用类型的本职行为。想隔离,就传副本或新建对象。

问:怎么快速判断一个类型是值类型还是引用类型? 简单记:基础数字类型、boolcharstruct、枚举是值类型;string、数组、class 实例是引用类型。更严谨地说,所有直接继承 System.ValueType 的都是值类型,其余引用类型继承 System.Object

要点速记

  • 值类型存”值本身”,赋值即复制、各改各的。
  • 引用类型存”地址”,赋值共享同一对象。
  • struct 默认值类型、class 默认引用类型。
  • 数组、字符串、类实例都是引用类型。
  • 引用类型默认值是 null,访问其成员当心空引用。

动手练一练

  1. 写两个 int 变量 ab,让 b = a 后修改 b,验证 a 不受影响(值类型)。
  2. 定义一个 Person 类(含 Name),p1 赋值后 p2 = p1,改 p2.Namep1.Name 是否同步(引用类型)。
  3. 把上面的 class Person 改成 struct Person,重跑同一段代码,观察行为差异并解释原因。

小结与下一步

值类型存”值本身”,赋值即复制、各改各的;引用类型存”地址”,赋值共享同一对象。struct 默认值类型、class 默认引用类型。这个区分贯穿你之后的所有 C# 代码,也决定了”你的修改会波及哪里”。

下一章我们把数值类型拆开看:整型和浮点型到底有什么不同,金额为什么非得用 decimal。