导读:本期聚焦于落伍者创作的《C#中record如何快速上手?不可变数据类型设计教程【速学】》,敬请观看详情。把 class 直接改成 record 就能获得不可变性吗?答案是否定的。record 的真正价值在于编译器生成的值相等逻辑以及 with 表达式带来的非破坏性复制能力。本文从基础声明讲起,先说明位置参数如何自动产生 init 访问器和 Deconstruct 方法,再演示如何用 with 只修改目标属性而不影响原对象。接着对比 record 与 class 在相等比较上的差异,包括继承场景下的 EqualityContract 判断,避免写出误判相等的业务逻辑。还会介绍 C# 10 引入的 record struct,解释在什么情况下选择值类型记录能降低堆分配。配套代码都经过语法检查,适合希望快速上手不可变数据模型设计的 C# 开发者。

C# 9 引入的 record 类型并不是简单地把 class 换了个名字,它在对象比较、复制和不可变建模上带来了一套全新的语义。很多开发者最初会把它当作只读类来用,实际上 record 最核心的能力包括基于值的相等性判断、配合 with 表达式实现非破坏性变更,以及编译器自动生成的一组成员。理解这些机制之后再动手写代码,能避免很多从传统 class 迁移时容易踩到的坑。

C#中record如何快速上手?不可变数据类型设计教程【速学】

如果只是把 class 关键字改成 record,却保留全部 set 访问器,那么对象仍然可以被随意修改,自然也就谈不上不可变模型。record 提供的是更符合领域建模习惯的语法和默认行为,而不是强制锁死数据。本文通过实际代码带你快速掌握 record 的声明方式、with 表达式、继承与值相等规则,以及 record struct 的性能取舍。

一、record 基础语法:位置记录与 init 访问器

record 最简洁的声明方式是使用位置参数,它能够让编译器自动生成主构造函数、公共属性以及 Deconstruct 方法。比如定义一个表示人员的记录,只需要一行代码。位置参数生成的属性默认使用 init 访问器,这意味着它们只能在对象初始化阶段通过构造函数或对象初始化器赋值,之后不允许再修改。

public record Person(string FirstName, string LastName);

var person = new Person("三", "张");
Console.WriteLine(person.FirstName); // 三
Console.WriteLine(person.LastName);  // 张

// person.FirstName = "李"; // 编译错误:init-only 属性不能在初始化后赋值

上面的位置记录等价于一个带有 init 属性、构造函数和 Deconstruct 方法的完整类型。init 访问器是 C# 中实现不可变属性的关键,它允许在对象初始化时写入,但对外部后续修改保持封闭。除了位置参数,你也可以手动声明普通属性和构造函数,效果相同,只是代码量会多一些。

不可变数据类型在线程安全、缓存复用和值对象建模中非常有用。多个线程可以安全地共享同一个 record 实例,因为谁都无法改变它的内部状态。对于需要频繁比较或复制的业务对象,record 能让代码更简洁,也更容易推理。

二、with 表达式:复制并修改指定属性

传统 class 要创建一个只修改某个属性的新对象,通常需要手动编写构造函数或对象初始化器,把所有属性重新列一遍。record 通过 with 表达式解决了这个问题。with 会以原对象为基础复制一份,然后只覆盖指定的属性值,原对象保持不变。

var original = new Person("三", "张");
var changed = original with { LastName = "李" };

Console.WriteLine(original.FirstName); // 三
Console.WriteLine(original.LastName);  // 张
Console.WriteLine(changed.FirstName);  // 三
Console.WriteLine(changed.LastName);   // 李

with 表达式实现的是浅拷贝。对于值类型成员或字符串这类不可变引用成员,不会产生额外问题。但如果 record 中包含可变的引用类型属性,复制后的新对象和原对象会引用同一个子对象。要避免这种情况,可以把嵌套对象也设计成 record,并在修改时逐层使用 with。

例如订单中包含地址信息,如果只想修改地址中的街道名称,而保留城市不变,可以这样处理。这种链式 with 写法在不可变模型里非常常见,既保证了原对象不被污染,又实现了局部更新。

public record Address(string City, string Street);
public record Employee(string Name, Address Address);

var emp = new Employee("王五", new Address("北京", "中关村大街"));
var moved = emp with
{
    Address = emp.Address with { Street = "上地" }
};

Console.WriteLine(emp.Address.Street);   // 中关村大街
Console.WriteLine(moved.Address.Street); // 上地

三、值相等与继承语义

record 默认重写了 Equals 和 GetHashCode,比较规则基于类型和所有属性值,而不是对象引用。这与 class 的默认引用相等完全不同。因此两个内容完全相同的 record 实例用 == 比较会返回 true,即使它们在堆上的地址不同。

var a = new Person("三", "张");
var b = new Person("三", "张");

Console.WriteLine(a == b);               // True
Console.WriteLine(a.Equals(b));          // True
Console.WriteLine(ReferenceEquals(a, b)); // False

需要注意的是,record 的相等性判断与运行时类型密切相关。编译器为每个 record 生成一个受保护的 EqualityContract 属性,用于标识当前类型。如果两个对象类型不同,即使基类属性完全一致,它们也不相等。这一点在继承场景中非常关键,否则可能出现业务上误判的风险。

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

Person p = new Person("三", "张");
Person s = new Student("三", "张", 2);

Console.WriteLine(p == s); // False
Console.WriteLine(s);      // Student { FirstName = 三, LastName = 张, Grade = 2 }

如果希望某个记录类型不能再被继承,避免派生类型干扰相等判断,可以在声明时加上 sealed 修饰符。record 还自动重写了 ToString 方法,打印出来的格式清晰可读,非常适合日志输出和调试观察。

四、record struct 与性能考量

C# 10 引入了 record struct,把 record 的值语义与值类型结合起来。传统 record class 是引用类型,每次创建实例都会在托管堆上分配内存。对于坐标、范围、消息头等小型数据结构,频繁创建会带来一定的垃圾回收压力。record struct 直接在栈上分配,避免了堆分配和后续回收开销。

public readonly record struct Point(int X, int Y);

var p1 = new Point(1, 2);
var p2 = new Point(1, 2);

Console.WriteLine(p1 == p2); // True
Console.WriteLine(p1.X);     // 1

record struct 同样支持位置参数、with 表达式和值相等比较,但它不能作为基类被继承,也没有引用相等概念。readonly record struct 可以进一步保证所有字段不可变,避免值类型复制过程中出现意外的修改行为。

选择 record class 还是 record struct,通常取决于数据规模和使用频率。如果对象需要传递、存储并共享给多个消费者,引用类型的 record class 更合适;如果只是短生命周期的小型数据载体,record struct 能带来更少的内存分配和更快的执行速度。两者并非替换关系,而是互补关系。

五、record 适用场景与迁移建议

record 非常适合表示领域模型中的值对象,比如货币金额、地址、日期范围、坐标点等。这些对象通常要求不可变,并且以值比较作为业务判断依据。在数据传输对象、命令行参数模型、事件消息等场景中,record 可以减少大量样板代码,让类型的意图更加明确。

从传统 class 迁移到 record 时,建议先梳理对象是否需要不可变性。如果类型仍有频繁修改属性的需求,或者非常依赖引用相等判断,那么继续使用 class 可能更合适。记录类型并不是为了让所有类都变成不可变,而是为真正需要值语义的对象提供更自然、更安全的表达方式。

C# record不可变数据类型with表达式修改时间:2026-09-27 08:23:25

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