记录类型 record
本教程共 100 篇 · 第 49 篇 · 更新于 2026-07-31 · 约 12 分钟阅读
49. 记录类型 record
本节目标:用
record获得值相等性、不可变属性和with拷贝,区分record class与record 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默认按”值”比较,更符合数据类型的直觉。判断相等用的是Equals与GetHashCode,编译器已为你正确生成。
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 同时比较 Name 与 Score。而 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 的对照
| 维度 | class | record |
|---|---|---|
| 相等判断 | 引用相等 | 值相等 |
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 时),可加普通方法、属性、运算符,甚至主构造器逻辑。它只是额外赠送了相等性、ToString、with。
问:为什么 EqualityContract 重要?
record 的相等性由类型守卫,派生类不会等于基类。底层靠一个 EqualityContract 属性区分类型,这也是”Student 不等于 Person”的原因。
问:该用 record 还是 class? 只装数据、要值相等就用 record;有独立身份(如用户、订单)或要可变状态就用 class。选错会让”两个数据相同却应是不同个体”的判断出错。
问:record struct 和 record class 性能差在哪?
record struct 在栈上、无堆分配;record class 在堆上、可被继承。小数据求性能用 struct,要继承层次用 class。
小结
record 把”数据即数据”这件事讲得很透:值相等、with 拷贝、漂亮输出三件套开箱即用。接下来我们看另一个底层话题:参数到底怎么传进方法。