Record 到底是什么?让我们消除困惑
把 record 想象成一个写有特定鸡尾酒及其配料的饮品菜单,而 class 则像是一所教你创造无限饮品变化的调酒学校。在深入技术细节之前,让我们先理解 record 要解决的问题:
使用传统的类方式 — 仅仅是为了保存一些数据就要写这么多代码!
public class PersonClass
{
public string FirstName { get; init; }
public string LastName { get; init; }
public PersonClass(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
// 需要实现相等性比较
public override bool Equals(object? obj)
{
if (obj is not PersonClass other) return false;
return FirstName == other.FirstName &&
LastName == other.LastName;
}
// 集合需要这个
public override int GetHashCode()
{
return HashCode.Combine(FirstName, LastName);
}
// 调试需要这个
public override string ToString()
{
return $"Person {{ FirstName = {FirstName}, LastName = {LastName} }}";
}
}
使用新的 record 方式 — 实现完全相同的功能!
public record PersonRecord(string FirstName, string LastName);
📝 我们将继续使用这个相同的类和记录示例!
为什么会有 Records?
record 的引入是因为开发者花费太多时间编写重复的代码来处理数据!开玩笑的.. 以下是使用 record 自动获得的功能:
不可变性
var person = new PersonRecord("John", "Doe");
person.FirstName = "Jane"; // 这行代码无法编译
// 相反,你需要创建一个带有更改的新记录:
var updatedPerson = person with { FirstName = "Jane" };
基于值的相等性比较(这很重要!)
使用类:
var person1 = new PersonClass("John", "Doe");
var person2 = new PersonClass("John", "Doe");
Console.WriteLine(person1 == person2); // False!不同的引用
使用 Records:
var record1 = new PersonRecord("John", "Doe");
var record2 = new PersonRecord("John", "Doe");
Console.WriteLine(record1 == record2); // True!相同的数据 = 相等
轻松复制并修改
var original = new PersonRecord("John", "Doe");
// 创建一个只改变 FirstName 的新记录:
var updated = original with { FirstName = "Jane" };
但是你知道吗!Records 稍微有点慢..让我们来看看
为什么 Records 会(稍微)慢一些?
与类相比,Records 有一点性能开销。但为什么会这样,以及为什么这通常并不重要:
// 基准测试:创建100万个实例
public class PerformanceComparison
{
private const int Iterations = 1_000_000;
[Benchmark]
public void CreateClasses()
{
for (int i = 0; i < Iterations; i++)
{
var person = new PersonClass("John", "Doe");
}
}
[Benchmark]
public void CreateRecords()
{
for (int i = 0; i < Iterations; i++)
{
var person = new PersonRecord("John", "Doe");
}
}
}
结果(近似值):类:~45ms || Records:~48ms
开销来自于:
生成的相等性方法
基于值的比较代码
额外的安全检查
现在,你一定在想为什么要不顾这些开销也要使用 Records?
为什么要不顾开销也要使用 Records...
开发者生产力
对于 API 响应,如果我们使用类,则需要大量代码:
public class ApiResponseClass<T>
{
public T Data { get; init; }
public bool Success { get; init; }
public string? Message { get; init; }
public DateTime Timestamp { get; init; }
// 需要构造函数
// 需要相等性比较
// 需要 ToString
// 需要哈希码
// 太多样板代码!
}
使用 record — 一行搞定!
public record ApiResponseRecord<T>(T Data, bool Success, string? Message, DateTime Timestamp);
不可变性 = 线程安全
因为 records 是不可变的,所以这是线程安全的:
public record Configuration(
string ApiKey,
string BaseUrl,
int Timeout
);
// 可以安全地在线程间共享
public class Service
{
private readonly Configuration _config;
public Service(Configuration config)
{
_config = config;
}
// 不需要锁 - 配置无法更改!
}
非常适合领域事件
Records 非常适合事件 — 它们是已发生的事实
public record OrderPlaced(
Guid OrderId,
string CustomerEmail,
decimal Amount,
DateTime PlacedAt
);
public record PaymentReceived(
Guid OrderId,
string TransactionId,
decimal Amount,
DateTime PaidAt
);
🚩 这些是不可变的事实 — 它们永远不应该改变!
该做与不该做
1. 深层 Record 层次结构可能会很慢
❌ 不要这样做:
public record Entity(Guid Id);
public record Person(Guid Id, string Name) : Entity(Id);
public record Employee(Guid Id, string Name, decimal Salary) : Person(Id, Name);
public record Manager(Guid Id, string Name, decimal Salary, string Department)
: Employee(Id, Name, Salary);
为什么?每次相等性检查都必须遍历整个层次结构!
✔️ 使用组合:
public record Manager(
Guid Id,
PersonInfo Person,
EmployeeInfo Employment,
string Department
);
2. 使用集合时要小心
❌ 问题代码:
public record UserList(List<User> Users)
{
public UserList AddUser(User user) =>
this with { Users = new List<User>(Users) { user } };
}
这每次都会创建一个新列表!
✔️ 更好的方式:
public class UserCollection
{
private readonly List<User> _users = new();
public IReadOnlyList<User> Users => _users.AsReadOnly();
public void AddUser(User user) => _users.Add(user);
}
让我们看看实际示例
1. API 契约
public record CreateUserRequest(
string Email,
string Password,
string FirstName,
string LastName
);
public record CreateUserResponse(
Guid UserId,
string Email,
DateTime CreatedAt
);
2. 领域事件
public record OrderShipped(
Guid OrderId,
string TrackingNumber,
DateTime ShippedAt,
Address ShippingAddress
);
3. 配置
public record DatabaseConfig(
string ConnectionString,
int MaxConnections,
TimeSpan Timeout,
bool EnableRetry
);
4. DDD中的值对象
public record Money(decimal Amount, string Currency)
{
public static Money Zero(string currency) => new(0, currency);
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return this with { Amount = Amount + other.Amount };
}
}
在我们结束本章之前,记住这个表格:
| **适合使用 Records 的场景** | **避免使用 Records 的场景** |
|--------------------------|------------------------|
| DTOs 和 API 契约 | 需要频繁更新的对象 |
| 配置对象 | 深层继承层次结构 |
| 领域事件 | 大型可变集合 |
| 值对象 | 复杂业务逻辑 |
| 任何不可变数据结构 | |
C# 中的 Records 不仅仅是语法糖 — 它们是以安全、不可变方式处理数据的强大工具。虽然它们带来了一些小的性能开销,但减少代码量、自动相等性比较和不可变性带来的好处通常远远超过了这些成本!
Records = 不可变数据容器
Classes = 带有行为的可变对象
根据需求选择,而不是根据性能
注意层次结构和集合的使用
现在你完全理解了何时以及为什么在 C# 应用程序中使用 records!
如果你喜欢我的文章,请给我一个赞!谢谢