深入理解 C# 中的 Record 类型
深入理解 C# 中的 Record 类型
总目录
前言
在 C# 9.0 中引入了 record 关键字,用于定义记录类型(Record Type),这是一种新的引用类型,旨在简化不可变数据类型的定义与操作。记录类型具有许多独特的特性,如值相等性、简洁的语法和内置的不可变性支持,使其成为处理数据传输对象(DTO)、配置类以及其他需要不可变性的场景的理想选择。本文将详细介绍 C# 中记录类型的定义、特性和最佳实践。
一、什么是 Record 记录类型
1. 定义
记录类型 是一种特殊的引用类型,主要用于表示不可变的数据结构。与普通类不同,记录类型提供了默认的值相等性比较,并且支持简化的构造函数和解构模式。
2. 基础示例
假设我们有一个简单的 Person 记录类型:
public record Person(string Name, int Age);
在这个例子中,Person 是一个记录类型,它包含两个属性:Name 和 Age。记录类型会自动生成构造函数、解构方法和其他必要的成员。
二、为什么需要 Record?
1. 传统类的局限
传统类的局限性在数据建模中愈发明显:
- 繁琐的样板代码:实现不可变类需手动定义只读属性、重写相等性方法
- 线程安全隐患:可变状态在多线程环境中易引发竞态条件
- 值语义缺失:类默认基于引用比较,数据相等性判断复杂
record应运而生,完美解决这些问题。它融合了引用类型的继承特性与值类型的相等语义,成为不可变数据模型的理想载体。
2. Record 的优势
- 简化代码:
record提供了一种简洁的方式来定义数据传输对象,减少了样板代码。 - 提高安全性:
record的不可变性确保了数据在传输过程中的安全性。 - 高可读性:
record的明确字段名称和简洁语法使得代码更加易于理解和维护。
三、Record 详解
1. 核心特性
1)简洁的语法
记录类型提供了一种简洁的语法来定义不可变的数据结构。你只需要指定属性名称和类型,编译器会自动生成相应的构造函数、解构方法和属性访问器。
-
编译器生成
ToString()(输出结构化的属性值)public record Person(string Name, int Age); class Program { static void Main() { var person = new Person("Alice", 30); Console.WriteLine(person); // 输出:Person { Name = Alice, Age = 30 } } } -
支持解构操作(可通过
Deconstruct方法提取属性)public record Person(string Name, int Age); class Program { static void Main() { var person = new Person("Alice", 30); //(var name, var age) = person; var (name, age) = person; // 解构为元组 Console.WriteLine(name); // 输出:Alice } } -
自动实现
init属性
2)值相等性
record 类型支持值相等性,即两个 record 对象的内容相等时,它们被认为是相等的。
public record Person(string Name, int Age);
class Program
{
static void Main()
{
var person1 = new Person("Alice", 30);
var person2 = new Person("Alice", 30);
Console.WriteLine(person1 == person2); // 输出: True
}
}
Record 的
Equals和==运算符通过属性值比较对象是否相等,而非引用地址。
这点与Class不同,Class 需要重写Equals和重载==运算符 才能实现 自定义对象属性值的比较,而Record自动实现Equals、GetHashCode以及重载==运算符。
public record Person(string Name, int Age);
class Program
{
static void Main()
{
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
var p3 = p2;
var p4 = new Person("Jack",30);
Console.WriteLine(p1 == p2); // 输出 True(值相等)
Console.WriteLine(p1 == p3); // 输出 True(值相等)
Console.WriteLine(p1 == p4); // 输出 False(值不相等)
Console.WriteLine(p1.Equals(p2));// 输出 True(值相等)
Console.WriteLine(p1.Equals(p3));// 输出 True(值相等)
Console.WriteLine(p1.Equals(p4));// 输出 False(值不相等)
Console.WriteLine(object.ReferenceEquals(p1,p2));// 输出 False(引用不相等)
Console.WriteLine(object.ReferenceEquals(p1,p3));// 输出 False(引用不相等)
Console.WriteLine(object.ReferenceEquals(p1,p4));// 输出 False(引用不相等)
}
}
上例展示了 record 类型 分别使用 Equals 方法和 == 运算符以及ReferenceEquals方法进行比较的区别。
3)不可变性(默认行为)
Record 类型默认属性为只读(get; init;),创建后不可修改,确保线程安全。
public record Person(string Name, int Age);
class Program
{
static void Main()
{
var person = new Person("Alice", 30);
//person.Name = "";//由于不可变性,当给其赋值时,编译错误
Console.WriteLine(person.Name);
}
}
4)with 表达式
基于现有实例创建新对象(类似函数式编程的"复制-修改"), Record 默认不可变,属性只能在初始化时赋值。若需修改,需通过 with 表达式创建新副本:
public record Person(string Name, int Age);
class Program
{
static void Main()
{
var person1 = new Person("Alice", 30);
var person2 = person1 with { Age = 31 }; // 生成新对象,保留其他属性。
Console.WriteLine(person1); // 输出: Person { Name = Alice, Age = 30 }
Console.WriteLine(person2); // 输出: Person { Name = Alice, Age = 31 }
}
}
5)模式匹配支持
结合 switch 实现基于属性的条件分支。
public record Person(string Name, int Age);
class Program
{
static void Main()
{
var person = new Person("Alice", 30);
switch (person)
{
case Person { Age: >= 18 } p: Console.WriteLine($"{p.Name} is Adult"); break;
case Person { Age: < 18 and > 0 } p: Console.WriteLine($"{p.Name} is Minor"); break;
}
}
}
6)继承与派生
记录类型支持继承和派生,但有一些限制。子类必须也是记录类型,并且基类的参数必须在子类的构造函数中传递。
public record Person(string Name, int Age);
public record Employee(string Name, int Age, string Department) : Person(Name, Age);
class Program
{
static void Main()
{
var employee = new Employee("Alice", 30, "Engineering");
Console.WriteLine(employee); // 输出: Employee { Name = Alice, Age = 30, Department = Engineering }
}
}
2. 语法变体
1)record class(默认)
- 引用类型:
record class或简写record。 - 引用类型,但基于值语义比较
public record Student(string Id, string Major);
2)record struct(C# 10+)
- 结构体类型:
record struct或readonly record struct。 - 值类型,适用于轻量级数据
public record struct Point(int X, int Y);
public readonly record struct Point(int X, int Y); // 不可变值类型。
值类型记录与引用类型记录的主要区别在于内存分配和复制行为。值类型记录在赋值时会进行深拷贝,而引用类型记录则共享引用。
3)属性自定义
可通过 init 访问器实现部分可变性(初始化后不可修改)。
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
class Program
{
static void Main()
{
var person = new Person() { Name = "Jack", Age = 15 };
person.Name = "Nan"; //初始化赋值后,更改会编译错误
}
}
3. 高级用法
1)自定义 Record
可扩展 Record 以添加方法或重写默认行为:
public record Person(string FirstName, string LastName)
{
public string FullName => $"{FirstName} {LastName}";
public override string ToString() => $"Person: {FullName}";
}
public record Person
{
public required string Name { get; init; }
public string Greet() => $"Hello, {Name}!";
}
2)可变 Record(不推荐)
通过 { get; set; } 定义可变属性,但违背 Record 设计初衷。
public record Person
{
public string Name { get; set; }
public int Age { get; set; }
}
3)特性标注
using System.Text.Json;
using System.Text.Json.Serialization;
public record Person(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("age")] int Age
);
class Program
{
static void Main()
{
var person = new Person("Jack",18);
var json = JsonSerializer.Serialize(person);
Console.WriteLine(json);//输出:{"name":"Jack","age":18}
}
}
4. 注意事项
- 若需要可变性,可显式声明
set访问器,但会破坏值相等语义 - 避免在
record中封装复杂业务逻辑,保持其数据容器的定位 - 深度嵌套对象时,
with表达式只执行浅拷贝
四、适用场景
1. DTO(数据传输对象)
在分布式系统中,记录类型可以用于定义请求和响应的数据结构,简化 API 请求/响应模型的创建。
public record CustomerDto(int Id, string Name, string Email);
2. 配置对象
在应用程序配置中,记录类型可以用来存储和传递配置信息(确保配置加载后不可篡改)。如数据库连接字符串。
public record AppConfig(string ConnectionString, int MaxRetries);
3. 不可变数据模型
记录类型非常适合用于需要保持数据一致性和不可变性的场景。
领域模型中的值对象(Value Object),确保数据不可变,避免副作用
4. 模式匹配
结合 switch 表达式实现高效数据匹配
var result = person switch
{
{ Age: >= 18 } => "Adult",
_ => "Minor"
};
public string GetPersonInfo(Person p) => p switch
{
{ Age: < 18 } => "Minor",
_ => "Adult"
};
五、Record 相关比较
1. Record 对比概览
| 特性 | Record | Class |
|---|---|---|
| 不可变性 | 默认不可变 | 默认可变 |
| 相等性比较 | 基于属性值 | 基于引用地址 |
| 语法简洁性 | 一行代码定义完整类型 | 需手动编写属性和方法 |
| 复制与修改 | 支持 with 表达式 |
需手动实现克隆逻辑 |
| 继承 | 支持继承其他 Record | 支持继承类或接口 |
ToString() |
自动生成属性摘要 | 返回类型名称 |
| 操作 | record class |
struct |
|---|---|---|
| 内存分配 | 堆分配 | 栈分配 |
| 相等性比较 | O(n)属性比较 | 内存逐字节比较 |
| 大对象传递 | 引用传递 | 值拷贝 |
优化建议:超过16字节的数据优先使用record class
| 特性 | 引用类型 record |
结构体 record struct |
|---|---|---|
| 值类型/引用类型 | 引用类型 | 值类型 |
| 属性可变性 | 默认不可变(init 可选) |
默认不可变(init 可选) |
| 继承支持 | 支持继承其他 record |
不支持继承 |
| 内存占用 | 较高(堆分配) | 较低(栈分配) |
2. 示例对比
1)简洁语法
Record 可通过一行代码定义包含多个属性的不可变类型。例如,定义一个表示“人”的 Record:
public record Person(string FirstName, string LastName);
编译器会自动生成构造函数、只读属性、基于值的相等性比较方法等。
2)传统类与 Record 的对比
传统类需要显式定义属性、构造函数和相等性方法,而 Record 将这些代码自动生成,减少样板代码量。例如:
// 传统类定义
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName) { ... }
// 需手动实现 Equals、GetHashCode 等
}
六、最佳实践
1. 保持简洁
记录类型应该保持简洁,专注于表示不可变的数据。避免在记录类型中添加复杂的业务逻辑或行为。
2. 使用场景
record适用于需要传输数据但不需要复杂业务逻辑的场景,例如数据传输对象(DTO)。- 虽然
record提供了便利,但在需要频繁修改对象状态的场景中,应避免使用。
3. 使用 init 访问器
如果你需要在对象初始化后允许某些属性被设置一次,可以使用 init 访问器。这样可以在保持不可变性的同时,允许有限的修改。
public record Person(string Name, int Age)
{
public string Address { get; init; }
}
class Program
{
static void Main()
{
var person = new Person("Alice", 30) { Address = "123 Main St" };
}
}
4. 结合 record 和普通类
在需要实现接口或复杂逻辑时,可以将 record 与普通类结合使用。
5. 处理复杂映射
当你的记录类型包含嵌套对象时,确保正确处理这些嵌套对象的映射。你可以使用 AutoMapper 或其他映射库来简化这一过程。
public record Address(string Street, string City, string State);
public record Person(string Name, int Age, Address Address);
class Program
{
static void Main()
{
var address = new Address("123 Main St", "New York", "NY");
var person = new Person("Alice", 30, address);
Console.WriteLine(person); // 输出: Person { Name = Alice, Age = 30, Address = Address { Street = 123 Main St, City = New York, State = NY } }
}
}
6. 版本控制
如果你的应用程序需要支持多个版本的 API,考虑为每个版本创建不同的记录类型。这样可以避免破坏现有客户端的兼容性。
public record PersonV1(string Name);
public record PersonV2(string Name, int Age);
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
Microsoft Docs: Records in C#
Best Practices for Using Records in C#
AutoMapper Documentation
更多推荐



所有评论(0)