导读:本期聚焦于徐致远创作的《C#如何使用Record类型构建不可变数据模型?》,敬请观看详情。为什么C#里越来越多的数据传输对象开始改用record而不是class?关键原因在于record把值相等和不可变初始化做成了默认行为,减少了大量样板代码。声明一个位置记录只需要一行,编译器会自动生成只读属性、构造函数、Equals、GetHashCode和ToString。基于init访问器的属性可以在对象初始化器中赋值,之后不能被随意修改。需要变更数据时,with表达式能基于现有实例创建副本,只替换指定字段。这种不可变数据模型在线程安全、状态可追踪、单元测试和缓存场景中优势明显。本文围绕record的声明方式、with表达式复制机制、值相等语义以及继承规则展开,并对比class与record struct的差异,帮助开发者把record用在实际项目中。文中还涉及与System.Text.Json等序列化工具的配合注意事项。

C# 从 9.0 开始引入 record 类型,它并不是简单增加一个新关键字,而是把不可变数据模型所需的样板代码沉淀到编译器中。对于需要表达数据快照、DTO、事件消息等场景,record 能显著减少属性声明、构造函数、相等性比较等重复代码。

C#如何使用Record类型构建不可变数据模型?

理解 record 的关键在于它默认采用值语义:两个 record 实例只要属性值相同就会被认为相等,而不是比较它们在内存中的引用地址。同时,record 的属性默认通过 init 访问器暴露,在完成初始化后不能随意修改,从而让对象状态更加稳定。下面从声明方式、复制机制、值相等和实际使用几个方面展开。

一、record 类型的声明与构造函数

最简洁的 record 声明方式是位置记录。只需要在 record 名称后的小括号中列出属性参数,编译器就会自动生成主构造函数、公开属性、Equals、GetHashCode、ToString 以及 Deconstruct 方法。例如下面一行代码就完成了一个包含两个不可变属性的人员模型。

public record Person(string FirstName, string LastName);

上面的代码等价于声明了一个带有 FirstName 和 LastName 两个只读属性的 record。编译器生成的主构造函数会把参数分别赋值给同名属性,同时生成基于属性值的相等性比较和格式化输出。调用方可以直接使用 new Person("San", "Zhang") 创建实例,也可以通过对象初始化器补充赋值。

如果需要自定义属性默认值或增加派生属性,也可以使用普通属性语法配合 init 访问器。init 允许在对象初始化器或 with 表达式中赋值,但在初始化完成后不允许修改。下面是自定义订单模型的写法。

public record Order
{
    public string Id { get; init; }
    public decimal Amount { get; init; }
    public string Currency { get; init; } = "CNY";
}

这种写法与 class 很像,但编译器仍会按 record 的值语义生成相等性比较和格式化输出。要注意 init 访问器只保护属性本身不可变,如果属性类型是 List 或其他引用类型,仍然可以修改其内部内容。要构建真正不可变的数据模型,应优先使用只读集合或不可变类型作为属性类型。

二、with 表达式如何复制并修改 record

不可变对象的一个常见问题是更新某个字段时必须创建新对象。record 提供了 with 表达式,让复制和局部修改变得非常直观。with 会调用编译器生成的复制构造函数,再执行对象初始化器中的赋值,因此原对象不会被改变。

var original = new Person("San", "Zhang");
var updated = original with { LastName = "Wang" };

Console.WriteLine(original);
Console.WriteLine(updated);

这里 updated 与 original 是两个独立实例,FirstName 相同,LastName 不同。with 表达式不是逐个字段手写复制,而是由编译器生成的 protected copy constructor 以及初始化器完成。它可以只写变化的部分,其他字段自动沿用原值,因此在包含大量属性的 record 中尤其方便。

需要注意 with 执行的是浅拷贝。如果 record 中某个属性是数组、List 或自定义引用类型,新对象和旧对象会共享同一个引用。例如 Address 属性指向同一个对象,修改 updated.Address.City 也会影响 original.Address.City。若要深拷贝,需要显式处理该属性的复制逻辑,或让属性类型本身不可变。

三、值相等与 record 继承规则

record 的默认相等性比较基于属性值而不是引用。C# 编译器会为 record 生成 Equals 和 GetHashCode,覆盖所有声明字段。这意味着 new Person("A", "B") 与另一个相同值的实例做比较时结果相等。

var p1 = new Person("A", "B");
var p2 = new Person("A", "B");

Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));

record 也可以继承自另一个 record。派生 record 会继承基类属性,同时编译器会生成 EqualityContract 来保证比较时运行时类型一致。如果基类实例和派生类实例属性相同,Equals 仍返回 false,因为它们是不同的 record 类型。

public record Person(string FirstName, string LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

位置记录的继承语法需要将参数传递给基类主构造函数。在上面的示例中 Student 同时包含三个属性。值比较会依次比较 FirstName、LastName 和 Grade,并检查 EqualityContract 是否匹配。这样继承体系下的相等性判断既考虑数据,也考虑具体运行时类型。

如果只想让类型拥有值语义但不需要引用类型分配,可以使用 C# 10 引入的 record struct。它也具有值相等和 with 表达式,但属于值类型,不支持继承。选择 record class 还是 record struct 主要看数据模型是否需要多态和引用语义。

四、record 在数据模型中的实践建议

record 非常适合作为 API 的请求响应 DTO、领域事件、配置快照和消息体。由于不可变,多线程读取时不需要加锁;由于值相等,可以用于断言和缓存键。配合 with 表达式,处理更新场景也很自然。下面是一个典型的接口响应模型。

public record ApiResponse(int Code, string Message, string TraceId);

在 System.Text.Json 中,record 可以正常序列化和反序列化。反序列化时,JSON 属性名需要与构造函数参数名匹配,且参数名不区分大小写。如果构造函数参数与 JSON 名称不一致,可以使用 JsonConstructor 或 JsonPropertyName 标注。这样可以避免因为不可变特性导致反序列化失败。

与 EF Core 协作时需要谨慎。数据库实体通常需要可变状态,以便跟踪更改,因此 record 不一定适合作为实体类型。不过对于无跟踪查询、临时投影或只读统计结果,record 可以简化查询结果的承载。用 EF Core 的 AsNoTracking 和 Select 投影到 record,能获得清晰不可变的读取模型。

在面向并发的场景中,record 提供的不可变默认行为可以减少共享状态的风险。但它不是银弹:引用类型属性仍可能引入间接修改。要充分利用不可变数据模型的优势,应把 record 设计成只包含基础类型、不可变集合或其他 record 类型的组合,这样才能让复制、比较和并发行为保持一致。

C# Record类型不可变数据模型with表达式修改时间:2026-08-27 07:25:50

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。