C# 9.0 引入的 record 类型通过简洁的语法为数据建模提供了新的范式。其核心设计目标是简化不可变数据实体的定义,通过自动实现的成员(如值相等性、ToString格式化和拷贝构造函数)提升开发效率。理解其特性并遵循最佳实践能帮助开发者避免常见陷阱,充分发挥其优势。
优先选择不可变性设计
Record 的默认行为强制不可变性,这是其区别于 class 的关键特性。当定义数据传输对象(DTO)、事件模型或配置参数时,应优先采用 init 访问器声明属性,确保对象在初始化后状态不可修改。这种设计天然支持线程安全并减少副作用,例如定义 API 响应模型:
public record WeatherForecast(
DateTime Date,
int TemperatureC,
string Summary
);
此声明自动生成密封类,包含只读属性、Equals、GetHashCode 和 ToString 的合理实现。若需允许部分属性后期修改,可显式定义可变属性,但需谨慎评估是否破坏数据一致性:
public record UserProfile(
Guid UserId,
string DisplayName
)
{
public DateTime LastLogin { get; set; } // 谨慎评估可变性需求
}
正确实现值语义
Record 默认通过结构比较(逐个字段对比)而非引用比较来实现相等性。当包含引用类型字段时,需确保其自身也实现值语义。例如,若记录包含集合类型,应选择不可变集合以避免意外修改:
public record Order(
string OrderId,
ImmutableList Items // 使用不可变集合
);
若必须使用可变集合,需重写 Equals 和 GetHashCode 方法实现深度比较。但这种情况应视为设计异味,建议重构为不可变设计。
合理利用非破坏性修改
with 表达式通过创建新实例实现非破坏性修改,适用于基于现有对象生成派生状态。例如,更新用户配置时保留原始对象不变:
var defaultSettings = new AppSettings(Theme: "Light", FontSize: 12);
var darkModeSettings = defaultSettings with { Theme = "Dark" };
注意 with 表达式执行浅拷贝。若记录包含引用类型字段,修改新对象的引用字段会影响原始对象。此时应结合不可变数据结构或实现深拷贝逻辑。
继承关系的设计考量
Record 支持继承,但可能引入复杂性。基类记录应声明为 abstract 并正确实现相等性逻辑,派生记录需显式调用基类构造函数:
public abstract record Vehicle(
string Make,
int Wheels
);
public record Car(
string Make,
int Wheels,
int Doors
) : Vehicle(Make, Wheels);
重写方法时需保持行为一致性。例如,重写 ToString 时应包含基类字段:
public override string ToString()
=> $"{base.ToString()},车门数:{Doors}";
建议优先使用组合而非继承,除非业务领域明确存在 is-a 关系。
模式匹配的协同应用
Record 的解构功能与模式匹配天然契合,可简化数据处理逻辑。结合 switch 表达式实现类型和属性分支处理:
var result = data switch
{
SuccessResponse(var value) => $"成功:{value}",
ErrorResponse(var code, _) when code >= 500 => "服务器错误",
ErrorResponse(_, var message) => $"客户端错误:{message}",
_ => "未知响应"
};
通过解构式模式可提取嵌套数据,避免冗长的属性访问链。建议为常用匹配模式实现 Deconstruct 方法。
序列化场景的注意事项
Record 的自动生成代码可能与某些序列化库存在兼容性问题。例如,JSON.NET 需要配置 ContractResolver 以支持构造函数反序列化:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
var json = JsonSerializer.Serialize(forecast, options);
var restored = JsonSerializer.Deserialize(json, options);
确保所有属性在构造函数中声明,否则反序列化可能失败。对于需要自定义序列化行为的场景,可显式实现 ISerializable 接口。
性能优化策略
虽然 record 的自动生成代码通常高效,但在高频创建场景需注意内存分配。对于大型记录(如超过 16 个字段),考虑使用 struct record 减少堆分配:
public record struct Point3D(
double X,
double Y,
double Z
);
但需权衡值类型的复制成本。通过 sealed 阻止派生可提升虚方法调用的性能,同时启用编译器优化。
遵循这些实践原则,开发者可以充分发挥 record 类型在数据封装、模式匹配和并发安全方面的优势,同时避免常见的误用模式。实际应用中应根据具体场景权衡设计选择,在类型安全与灵活性之间取得平衡。