为什么存在Records?|Records vs class |完整的开发人员决策指南

科技   2024-12-08 06:41   上海  


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

开销来自于:

  1. 生成的相等性方法

  2. 基于值的比较代码

  3. 额外的安全检查

现在,你一定在想为什么要不顾这些开销也要使用 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!

如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章