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

作者:微信公众号:【架构师老卢】
11-16 19:10
113

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!

相关留言评论
昵称:
邮箱:
阅读排行