Record、Class 和 Struct 的选择指南
在 C# 开发中,正确选择数据类型对于代码的性能、可维护性和安全性至关重要。本文将详细解析何时使用 record
、class
和 struct
,并提供实用的选择指南。
问题背景
在开发过程中,我们经常面临数据类型的选择问题:
- 是否应该为控制器和服务层之间的所有 DTO 类使用
Record
? - 是否应该为所有请求绑定使用
Record
,以实现 API 请求的不可变性? SearchParameters
这样的参数类型是否应该定义为Record
?
核心概念对比
Class(类)
- 引用类型,存储在堆上
- 适用于定义对象的职责和行为
- 支持继承、多态和接口实现
- 允许非公共的无参构造函数
Struct(结构体)
- 值类型,通常存储在栈上
- 适用于小型数据结构(实例大小小于 16 字节)
- 逻辑上表示单个值,类似于基本类型
- 应该是不可变的
- 不支持继承
Record(记录)
- 默认不可变的引用类型
- 提供基于值的相等性比较
- 简洁的语法创建不可变类型
- 支持非破坏性变化的
with
表达式 - 内置格式化显示支持
选择指南
何时使用 Struct?
Struct 适用条件
您的数据类型应满足以下所有条件:
- 逻辑上表示单个值,类似于基本类型(int、double 等)
- 实例大小小于 16 字节
- 是不可变的
- 不需要频繁装箱
如果满足以上条件,选择 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 μsrecord
:120.19 μs
虽然 record
有性能开销,但在大多数应用场景中影响不大。
实践建议
DTO 和请求绑定
✅ 推荐使用 record
:
- API 请求/响应模型
- 服务间数据传输对象
- 任何需要不可变性的数据载体
领域模型
✅ 推荐使用 class
:
- 需要丰富行为和业务逻辑的领域实体
- 需要精细封装和验证的复杂对象
值对象
✅ 根据具体情况选择:
- 简单值对象:考虑
readonly struct
(C# 9+) - 复杂值对象:考虑
record
或自定义值对象实现
总结
选择数据类型的关键问题:
- 能否是值类型? → 是:
struct
- 是否需要值语义和不可变性? → 是:
record
- 是否需要丰富的行为和继承? → 是:
class
对于大多数数据传输和不可变数据场景,record
提供了最佳的语法简洁性和安全性平衡。对于问题中的 SearchParameters
,使用 record
是最合适的选择。
::: success 最终建议
- DTO 和请求绑定:使用
record
- 简单数值数据:使用
struct
- 复杂业务对象:使用
class
- 性能关键场景:考虑
readonly struct
或record struct
:::
通过合理选择数据类型,您可以编写出更安全、更清晰且更高效的 C# 代码。