Skip to content

Record、Class 和 Struct 的选择指南

在 C# 开发中,正确选择数据类型对于代码的性能、可维护性和安全性至关重要。本文将详细解析何时使用 recordclassstruct,并提供实用的选择指南。

问题背景

在开发过程中,我们经常面临数据类型的选择问题:

  • 是否应该为控制器和服务层之间的所有 DTO 类使用 Record
  • 是否应该为所有请求绑定使用 Record,以实现 API 请求的不可变性?
  • SearchParameters 这样的参数类型是否应该定义为 Record

核心概念对比

Class(类)

  • 引用类型,存储在堆上
  • 适用于定义对象的职责和行为
  • 支持继承、多态和接口实现
  • 允许非公共的无参构造函数

Struct(结构体)

  • 值类型,通常存储在栈上
  • 适用于小型数据结构(实例大小小于 16 字节)
  • 逻辑上表示单个值,类似于基本类型
  • 应该是不可变的
  • 不支持继承

Record(记录)

  • 默认不可变的引用类型
  • 提供基于值的相等性比较
  • 简洁的语法创建不可变类型
  • 支持非破坏性变化的 with 表达式
  • 内置格式化显示支持

选择指南

何时使用 Struct?

Struct 适用条件

您的数据类型应满足以下所有条件:

  1. 逻辑上表示单个值,类似于基本类型(int、double 等)
  2. 实例大小小于 16 字节
  3. 是不可变的
  4. 不需要频繁装箱

如果满足以上条件,选择 struct

何时使用 Record?

Record 适用场景

  • 数据类型封装了复杂的值
  • 值是默认不可变的
  • 用于单向数据流(如 DTO、请求绑定)
  • 需要基于值的相等性比较
  • 需要非破坏性变化功能

对于问题中的示例:

csharp
public async Task<IActionResult> Search(SearchParameters searchParams)
{
    await _service.SearchAsync(searchParams);
}

SearchParameters理想的 record 使用场景,因为它:

  • 作为 API 请求参数应该是不可变的
  • 通常包含多个属性构成一个逻辑数据单元
  • 需要从客户端传递到服务端

何时使用 Class?

Class 适用场景

  • 需要定义对象的行为和职责
  • 需要继承或多态特性
  • 数据类型是可变的
  • 需要精细控制封装性

Record 的进阶特性

可变性控制

虽然 record 默认是不可变的,但您可以使其部分可变:

csharp
record Foo(string Bar)
{
    internal double MutableProperty { get; set; } = 10.0;
}

复制行为解析

record 的复制行为需要注意:

csharp
var original = new SomeRecord(new List<string>());
var copy = original with { };

original.List.Add("test");
// copy.List 也会包含 "test",因为引用类型成员是浅拷贝

复制注意事项

  • with 表达式创建的是新引用
  • 值类型成员被复制,引用类型成员指向同一引用
  • 只有全部是值类型属性的记录才能实现真正的深拷贝

性能考量

在性能关键场景中(如游戏、VR/AR、云服务),需要考虑不同类型的内存分配和性能特征:

  • struct:栈分配,性能最佳但受大小限制
  • record:堆分配,有 GC 开销但功能丰富
  • class:堆分配,最灵活但性能开销最大
性能测试结果对比

基准测试显示,在大量创建和修改场景中:

  • class:98.91 μs
  • record:120.19 μs

虽然 record 有性能开销,但在大多数应用场景中影响不大。

实践建议

DTO 和请求绑定

推荐使用 record

  • API 请求/响应模型
  • 服务间数据传输对象
  • 任何需要不可变性的数据载体

领域模型

推荐使用 class

  • 需要丰富行为和业务逻辑的领域实体
  • 需要精细封装和验证的复杂对象

值对象

✅ 根据具体情况选择:

  • 简单值对象:考虑 readonly struct(C# 9+)
  • 复杂值对象:考虑 record 或自定义值对象实现

总结

选择数据类型的关键问题:

  1. 能否是值类型? → 是:struct
  2. 是否需要值语义和不可变性? → 是:record
  3. 是否需要丰富的行为和继承? → 是:class

对于大多数数据传输和不可变数据场景,record 提供了最佳的语法简洁性和安全性平衡。对于问题中的 SearchParameters,使用 record 是最合适的选择。

::: success 最终建议

  • DTO 和请求绑定:使用 record
  • 简单数值数据:使用 struct
  • 复杂业务对象:使用 class
  • 性能关键场景:考虑 readonly structrecord struct :::

通过合理选择数据类型,您可以编写出更安全、更清晰且更高效的 C# 代码。