前言
大家都知道,类(class)和接口(interface)是面向对象开发的两大支柱,但是在 C# 9.0,.NET 团队又引入了一种全新的类型: 记录(record)类型。它究竟是何方神圣?跟传统的类(class)类型有什么区别?它会颠覆面向对象的开发方式吗?……
今天我们来聊聊这个记录(record)类型,及它跟类(class)类型的区别和联系。
记录(record)类型是什么?
记录(record)类型是 .NET 团队设计出来的一个用于定义不可变数据模型的特殊类型。
所谓不可变,就是说,这个类型一旦被创建,它里面的属性的值就不能被修改。
所有属性、成员变量都为只读的类型叫作 "不可变类型",不可变类型可以简化程序逻辑,并且可以减少并发访问、状态管理等麻烦。
它如何定义呢?来看一个简单的记录(record)类型的使用例子。
一个简单的例子
在这个例子里,定义了一个 Person
记录类型,和两个不可变属性:FirstName
和LastName
。
using System; // 定义一个记录(record)类型,及两个不可变属性 public record Person(string FirstName, string LastName); class Program { static void Main() { // 创建一个 Person 实例 var person1 = new Person("Jacky", "Yang"); // 以下语句会出错,不可以在运行时改变 person1 对象的属性值 // person1.FirstName = "Meng"; // 打印对象 Console.WriteLine(person1); // 结果: Person { FirstName = Jacky, LastName = Yang } // 创建与 person1 相同的新实例 var person2 = person1 with { }; // 检查相等性 Console.WriteLine(person1 == person2); // 输出: True // 创建一个新对象,修改属性 var person3 = person1 with { LastName = "Smith" }; Console.WriteLine(person3); // 输出: Person { FirstName = John, LastName = Smith } } }
优势
从上面的例子可以看到,记录(record)类型具有这些优势:
- 记录(record)类型的定义非常简单,仅需 1 行代码
- 两个相同属性的记录(record)类型对象可以直接比较是否相等
- 记录(record)类型的属性只能在定义时赋值,不能在运行中改变,这种不可变性,可以有效防止意外的数据更改,减少错误;在多线程环境中,也会带来更高的性能。
跟类(class)类型的区别
- 不可变性:
record
默认是不可变的,这意味着一旦创建了一个record
实例,就不能修改它的属性值。class
没有这样的限制,你可以随意更改其属性值。
- 默认的构造函数:
record
有一个默认的参数化(为所有属性赋值)的构造函数,可以通过初始化器设置所有属性值。class
没有这样的默认构造函数,需要显式声明。
- 值相等:
record
通过值比较来判断两个对象是否相等,这意味着两个具有相同属性值的record
实例会被认为是相等的。class
则是通过引用比较来判断相等性,如果要判断同一个类型的两个实例对象是否相等,需要通过重写 Equals 方法、重写 == 运算符等来解决这个问题,总之需要编写非常多的额外代码,比较麻烦。
- 解构:
record
支持解构,可以直接拆分出其成员。class
需要显式实现解构逻辑。
应用场景
- 数据传输对象(DTO): 在 ASP.NET Web API 开发中,记录(record)类型很适合用来定义 DTO 数据模型,因为记录(record)类型天然具有 DTO 数据模型所要求的简单、不可变、轻量级、容易序列化等特性
- 领域模型: 在领域驱动设计中,记录(record)类型非常适合用来表示值对象
- 其他: 其它要求确保实体状态不变的业务场景
本质
记录(record)类型有这么多优点,看起来它跟类(class)类型有很多不同,但相似之处更多,它的本质究竟是什么呢?
反编译上面例子生成的程序集,可以看到,编译器把记录(record)类型的 Person
类型编译成一个 Person
类,并且提供了构造方法、属性、ToString方法、Equals方法等,所以记录(record)类型的本质其实依然是一个类(class)类型,它并没有颠覆面向对象的开发方式,它实际上只是一个语法糖。
以下是记录(record)类型反编译后的主要代码:
public class Person : IEquatable<Person> { public string FirstName { get; set /*init*/; } public string LastName { get; set /*init*/; } public Person(string FirstName, string LastName) { this.FirstName = FirstName; this.LastName = LastName; } public override string ToString() { //省略代码 } public virtual bool Equals(Person? other) { //省略代码 } }
注意事项
- 既然记录(record)类型本质也是一个类(class)类型,就意味它也可以定义可变属性,但这是不建议的,因为这跟记录(record)类型的设计理念相冲突,如果需要频繁修改的状态,
class
类型更为合适。 - 对于记录(record)类型的更新,需要谨慎设计以避免破坏向后兼容性
总结
记录(record)类型提供了为所有属性赋值的构造方法,所有属性都是只读的,对象之间可以进行值的相等性比较,并且编译器为类型提供了可读性强的 ToString 方法。
它结合了值类型和引用类型的特性,特别适合用来表示那些不可变的数据结构,比如数据传输对象(DTO)或领域模型中的值对象。
在需要编写不可变类并且需要进行对象值比较的时候,使用记录(record)类型可以把代码的编写难度大大降低。
记录(record)类型相比类(class)类型,有很多不同的地方,但它本质上也是一个类(class)类型,这也意味着它可以做到类(class)类型可以做的事情,所以在使用时尤其要注意其边界,在正确的场景中使用它,才能有化腐朽为神奇的效果。
我是老杨,一个执着于编程乐趣、至今奋斗在一线的 10年+ 资深研发老鸟,是软件项目管理师,也是快乐的程序猿,持续免费分享全栈实用编程技巧、项目管理经验和职场成长心得。欢迎关注老杨的公众号,相互交流,共同进步!