首页 / C# 入门教程 / 记录类型 record

C# 入门教程

记录类型 record

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

C#C# 入门教程面向对象record

49. 记录类型 record

本节目标:用 record 获得值相等性、不可变属性和 with 拷贝,区分 record classrecord struct

为什么需要 record

普通 class 比的是”引用是否同一对象”。但很多类型(如坐标、配置)我们只关心”数据是否相等”。传统写法要手写 Equals/GetHashCode/ToString,啰嗦易错。record 让编译器自动生成这些成员。

可以说,record 是为”以数据为中心”的类型量身定做的语法糖。它把”值相等""漂亮输出""拷贝修改”三件事一次性办妥。

Tip

一句话判断:类型”主要是数据”就用 record;类型”主要行为/有身份”就用 class。比如”人”有独立身份用 class,“两点坐标”只认数据用 record

值相等性

两个 record 只要类型和每个字段的值都相同,就判定相等,而不是看是不是同一个对象。

namespace CSharpDemo;

record Person(string Name, int Age);

Person a = new("小明", 18);
Person b = new("小明", 18);
Console.WriteLine(a == b); // True,值相等
Note

普通 class 这里会输出 False(引用不同);record 默认按”值”比较,更符合数据类型的直觉。判断相等用的是 EqualsGetHashCode,编译器已为你正确生成。

with 表达式:非破坏性修改

想改一个字段又不想动原对象?with 会复制一份并替换指定属性,原对象保持不动。

namespace CSharpDemo;

record Person(string Name, int Age);

Person p1 = new("小明", 18);
Person p2 = p1 with { Age = 19 }; // 拷贝并改 Age
Console.WriteLine(p1); // Person { Name = 小明, Age = 18 }
Console.WriteLine(p2); // Person { Name = 小明, Age = 19 }

这种”改副本不改原件”的特性叫非破坏性修改(nondestructive mutation),非常适合不可变数据。在并发场景里,只读数据天然线程安全。

位置记录与完整定义

record Person(string Name, int Age); 是位置记录(positional record),主构造器直接生成属性、构造器和 ToString。也可以写成常规属性块:

namespace CSharpDemo;

record Product
{
    public string Name { get; init; } = "";
    public decimal Price { get; init; }
}

Product p = new() { Name = "书", Price = 9.99m };
Console.WriteLine(p);

init 访问器(C# 9)让属性只能在初始化时赋值,之后只读,进一步保证不可变。

解构与模式匹配

位置记录会自动生成 Deconstruct,可把字段拆到变量里;配合模式匹配用起来很顺。

namespace CSharpDemo;

record Person(string Name, int Age);

var (name, age) = new Person("小明", 18);
Console.WriteLine($"{name} 今年 {age}");

if (new Person("小明", 18) is Person("小明", var a))
    Console.WriteLine($"年龄是 {a}");

record 也能继承

record class 支持继承,派生记录会继承并参与值比较。

namespace CSharpDemo;

record Person(string Name);
record Student(string Name, int Score) : Person(Name);

Student s1 = new("小明", 90);
Student s2 = new("小明", 90);
Console.WriteLine(s1 == s2); // True,值与类型都相同

Student 同时比较 NameScore。而 record struct 不支持继承。继承时,值相等还要求”类型相同”,派生类不会等于基类——一个 Student 永远不会等于一个 Person,即使名字相同。

record class 与 record struct(C# 10)

record 默认是引用类型(record class)。C# 10 起还能定义 record struct,它是值类型,同样享受值相等与 with

namespace CSharpDemo;

record struct Point(int X, int Y);

Point p1 = new(1, 2);
Point p2 = p1 with { Y = 3 };
Console.WriteLine(p1 == p2); // False,Y 不同
Tip

小数据、想要值语义且避免堆分配,用 record struct;需要继承或当引用类型,用 record class

record 的 ToString 长什么样

编译器生成的 ToString 会打印类型名和每个公共属性的值,例如 Person { Name = 小明, Age = 18 }。调试时一眼看清数据,省去自己拼字符串。

不可变带来的线程安全

位置记录默认不可变这件事,值得多说一句。因为数据一旦创建就不能改,多个线程同时读它时不必加锁,天然线程安全。这也是函数式编程偏爱”不可变数据”的原因——你不用追着”谁在哪一刻改了它”,程序行为更可预测。with 表达式在此之上又补了一层:只读数据要”改”时,它生成新副本而非动原件,既拿到新值又保住原数据的不变性。

Tip

想让 record 的某个字段可变?可以显式写带 set 的属性,但请三思。可变会破坏”值语义”带来的安全感与可预测性,多数记录类型保持不可变更稳妥。除非业务确实需要,否则让数据”生来定型”。

record 与 class 的对照

维度classrecord
相等判断引用相等值相等
ToString类型名字段明细
with 拷贝需手写内置
是否可继承record class 可,record struct 不可
适用有身份/行为的对象以数据为中心

新手常见坑

第一,以为 record 自动不可变。位置记录的主构造参数默认是只读的,但如果你自己加 set 属性,仍然可改。不可变要主动设计。

第二,在需要”同一对象身份”的场景错用 record。若业务上两个数据相同也应被视为不同个体(如两个订单),用 class 更合适。

第三,record struct 不支持继承。需要继承层次时请用 record class

第四,误以为 with 是”修改原对象”。with 永远返回新实例,原对象不受影响。

Warning

record 的值相等是”浅比较”:它只比较直接字段。若字段本身是引用类型,比较的是那些对象的引用或它们自己的 Equals——务必保证嵌套类型也正确实现了相等性,否则会出现”看起来一样却不相等”的怪象。

record 与集合:值相等真好用

因为 record 天生值相等,把它当字典键或放进集合时,“数据相同”就被当成”同一个”。这省去手写 Equals/GetHashCode 的麻烦。

namespace CSharpDemo;

record Point(int X, int Y);

Dictionary<Point, string> map = new()
{
    [new Point(1, 2)] = "起点"
};

Console.WriteLine(map[new Point(1, 2)]); // 起点,值相同即命中

若用普通 class,不重写相等性的话,上面这行会找不到键而抛异常。record 让”按数据查”变得理所当然。

从 class 迁移到 record 时,注意语义变化:以前”两个对象即使数据相同也是不同个体”,现在”数据相同即相等”。若你的业务恰恰依赖”独立身份”(如两张同金额订单是不同的),改用 record 反而会造成逻辑错误。动手前先问自己:这个类型比的是身份,还是数据?

常见疑问解答

问:record 的相等比较是深比较吗? 是”浅”比较:只比直接字段。若字段本身是引用类型,比较的是那些对象的引用或其自身 Equals。嵌套类型也要正确实现相等,结果才符合直觉。

问:位置记录和普通属性块 record 有啥区别? 位置记录用主构造器自动生成属性、构造器和 Deconstruct;属性块写法更灵活,可自己控制访问器(如 init)。本质都是 record,值相等都生效。

问:record 能像 class 一样写方法吗? 能。record 仍是类(record class 时),可加普通方法、属性、运算符,甚至主构造器逻辑。它只是额外赠送了相等性、ToStringwith

问:为什么 EqualityContract 重要? record 的相等性由类型守卫,派生类不会等于基类。底层靠一个 EqualityContract 属性区分类型,这也是”Student 不等于 Person”的原因。

问:该用 record 还是 class? 只装数据、要值相等就用 record;有独立身份(如用户、订单)或要可变状态就用 class。选错会让”两个数据相同却应是不同个体”的判断出错。

问:record struct 和 record class 性能差在哪? record struct 在栈上、无堆分配;record class 在堆上、可被继承。小数据求性能用 struct,要继承层次用 class。

小结

record 把”数据即数据”这件事讲得很透:值相等、with 拷贝、漂亮输出三件套开箱即用。接下来我们看另一个底层话题:参数到底怎么传进方法。