Record 类型与不可变数据
大约 10 分钟约 3105 字
Record 类型与不可变数据
简介
Record 是 C# 9.0 引入的引用类型,专门用于不可变数据模型。它基于值语义进行相等性比较,内置 with 表达式支持非破坏性修改。Record 让数据对象的定义变得极其简洁,是 DDD 中值对象和 DTO 的理想选择。
特点
基本用法
Record 定义方式
/// <summary>
/// Record 的多种定义方式
/// </summary>
// 方式1:位置记录(最简洁)
public record Person(string Name, int Age);
// 方式2:标准记录(可添加额外成员)
public record User
{
public string Name { get; init; }
public string Email { get; init; }
public int Age { get; init; }
public User(string name, string email, int age)
{
Name = name;
Email = email;
Age = age;
}
}
// 方式3:结构体记录(值类型,栈分配)
public record struct Point(double X, double Y);
// 方式4:可变记录(不推荐,但可以)
public record MutableData
{
public string Value { get; set; } // 可变属性
}值语义相等
/// <summary>
/// Record 的核心特性 — 值相等
/// </summary>
var person1 = new Person("张三", 28);
var person2 = new Person("张三", 28);
// class:false(引用比较)
// record:true(值比较)
Console.WriteLine(person1 == person2); // True
Console.WriteLine(person1.Equals(person2)); // True
Console.WriteLine(ReferenceEquals(person1, person2)); // False
// GetHashCode 也基于值
Console.WriteLine(person1.GetHashCode() == person2.GetHashCode()); // True
// 自动生成的 ToString
Console.WriteLine(person1); // Person { Name = 张三, Age = 28 }with 表达式 — 非破坏性修改
/// <summary>
/// with 表达式 — 创建副本并修改部分属性
/// </summary>
var original = new Person("张三", 28);
// 创建新副本,修改 Age
var updated = original with { Age = 29 };
Console.WriteLine(original); // Person { Name = 张三, Age = 28 }
Console.WriteLine(updated); // Person { Name = 张三, Age = 29 }
// 链式修改
var transformed = original
with { Age = 30 }
with { Name = "李四" };
// 实际应用 — 更新 DTO
var request = new UpdateUserRequest("张三", "zhangsan@test.com", 28);
var updated2 = request with { Email = "newemail@test.com" };Record 继承
/// <summary>
/// Record 支持继承
/// </summary>
public abstract record Shape(string Name)
{
public abstract double Area();
}
public record Circle(string Name, double Radius) : Shape(Name)
{
public override double Area() => Math.PI * Radius * Radius;
}
public record Rectangle(string Name, double Width, double Height) : Shape(Name)
{
public override double Area() => Width * Height;
}
// 使用
Shape shape = new Circle("圆形A", 5);
Console.WriteLine(shape); // Circle { Name = 圆形A, Radius = 5 }
// with 保持子类型
Circle circle = new Circle("圆形", 10);
Circle bigger = circle with { Radius = 20 }; // 仍然是 Circle 类型实际应用场景
1. DTO 和 API 请求/响应
/// <summary>
/// Record 是 DTO 的最佳选择
/// </summary>
public record CreateUserRequest(
string UserName,
string Email,
string Phone,
int DepartmentId
);
public record UserResponse(
int Id,
string UserName,
string Email,
string DepartmentName,
DateTime CreatedTime
);
public record PagedResult<T>(List<T> Items, int TotalCount, int Page, int PageSize);
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpPost]
public ActionResult<UserResponse> Create(CreateUserRequest request)
{
// Record 的解构让参数传递很方便
var user = CreateUser(request);
return Ok(user);
}
}2. 领域事件
/// <summary>
/// Record 天然适合领域事件 — 不可变、值语义
/// </summary>
public abstract record DomainEvent(DateTime OccurredOn)
{
protected DomainEvent() : this(DateTime.Now) { }
}
public record OrderCreatedEvent(
Guid OrderId,
Guid UserId,
decimal TotalAmount
) : DomainEvent;
public record OrderPaidEvent(
Guid OrderId,
decimal Amount,
string PaymentMethod
) : DomainEvent;
public record OrderShippedEvent(
Guid OrderId,
string TrackingNumber,
string Carrier
) : DomainEvent;3. 配置选项
/// <summary>
/// Record 用于不可变配置
/// </summary>
public record JwtOptions(
string SecretKey,
string Issuer,
string Audience,
int ExpireMinutes
)
{
// 可以添加验证方法
public void Validate()
{
if (string.IsNullOrWhiteSpace(SecretKey) || SecretKey.Length < 16)
throw new InvalidOperationException("JWT密钥至少16个字符");
}
}
public record DatabaseOptions(
string ConnectionString,
int MaxRetryCount,
int CommandTimeoutSeconds
)
{
public static DatabaseOptions Default => new("", 3, 30);
}4. Result 模式
/// <summary>
/// 使用 Record 实现 Result 模式
/// </summary>
public abstract record Result<T>
{
public static Result<T> Success(T value) => new SuccessResult<T>(value);
public static Result<T> Failure(string error) => new FailureResult<T>(error);
public abstract TResult Match<TResult>(
Func<T, TResult> onSuccess,
Func<string, TResult> onFailure);
}
public record SuccessResult<T>(T Value) : Result<T>
{
public override TResult Match<TResult>(
Func<T, TResult> onSuccess,
Func<string, TResult> onFailure) => onSuccess(Value);
}
public record FailureResult<T>(string Error) : Result<T>
{
public override TResult Match<TResult>(
Func<T, TResult> onSuccess,
Func<string, TResult> onFailure) => onFailure(Error);
}
// 使用
public Result<User> FindUser(int id)
{
var user = _repository.GetById(id);
return user != null
? Result<User>.Success(user)
: Result<User>.Failure("用户不存在");
}
var result = FindUser(1);
var message = result.Match(
user => $"找到用户:{user.Name}",
error => $"错误:{error}"
);5. Record struct — 高性能值类型
/// <summary>
/// Record struct — 值类型 Record,适合小型数据结构
/// </summary>
public record struct Money(decimal Amount, string Currency)
{
public static Money Zero(string currency = "CNY") => new(0, currency);
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("币种不同不能相加");
return this with { Amount = Amount + other.Amount };
}
public Money Multiply(int factor) => this with { Amount = Amount * factor };
}
public record struct DateRange(DateTime Start, DateTime End)
{
public int Days => (End - Start).Days;
public bool Contains(DateTime date) => date >= Start && date <= End;
}
// 栈分配,无 GC 压力
var price = new Money(99.9m, "CNY");
var total = price.Multiply(3);
Console.WriteLine(total); // Money { Amount = 299.7, Currency = CNY }Record vs Class vs Struct
| 特性 | Record | Record Struct | Class | Struct |
|---|---|---|---|---|
| 类型 | 引用类型 | 值类型 | 引用类型 | 值类型 |
| 相等性 | 值比较 | 值比较 | 引用比较 | 值比较 |
| 不可变 | 默认 | 默认 | 需手动 | 需手动 |
| with | 支持 | 支持 | 不支持 | 不支持 |
| 继承 | 支持 | 不支持 | 支持 | 不支持 |
| 适用 | DTO/事件/值对象 | 小型值类型 | 业务实体 | 性能敏感 |
Record 底层原理
编译器生成的成员
/// <summary>
/// Record 编译器自动生成的成员
/// </summary>
// 位置 record:public record Person(string Name, int Age);
// 编译器生成以下成员:
// 1. 主构造函数
public Person(string Name, int Age) { this.Name = Name; this.Age = Age; }
// 2. 属性(init only)
public string Name { get; init; }
public int Age { get; init; }
// 3. EqualityContract — 判断类型是否相同
protected virtual Type EqualityContract => typeof(Person);
// 4. Equals 方法 — 值比较
public override bool Equals(object? obj)
{
return obj is Person other && EqualityContract == other.EqualityContract
&& Name == other.Name && Age == other.Age;
}
public virtual bool Equals(Person? other)
{
return other is not null && EqualityContract == other.EqualityContract
&& Name == other.Name && Age == other.Age;
}
// 5. GetHashCode — 基于所有属性
public override int GetHashCode() => HashCode.Combine(EqualityContract, Name, Age);
// 6. == 和 != 运算符
public static bool operator ==(Person? left, Person? right)
=> (left is null && right is null) || (left is not null && left.Equals(right));
public static bool operator !=(Person? left, Person? right) => !(left == right);
// 7. ToString — 属性输出
public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }}";
// 8. PrintMembers — ToString 使用的方法
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append($"Name = {Name}, Age = {Age}");
return true;
}
// 9. Deconstruct 方法 — 解构支持
public void Deconstruct(out string Name, out int Age)
{
Name = this.Name;
Age = this.Age;
}
// 10. with 表达式 — 使用 <Clone>$ 方法
public virtual Person <Clone>$()
{
return new Person(this);
}
// with 表达式编译为:
// var updated = original.<Clone>$();
// updated.Name = "NewName"; // 修改指定属性Record 的 with 表达式原理
/// <summary>
/// with 表达式的编译器行为
/// </summary>
// var updated = original with { Age = 29 };
// 编译器生成:
// var temp = original.<Clone>$();
// temp.Age = 29;
// var updated = temp;
// 对于 record class:<Clone>$ 创建浅拷贝
// 注意:引用类型字段是浅拷贝(共享引用)
// 浅拷贝陷阱
public record PersonWithFriends(string Name, List<string> Friends);
var original = new PersonWithFriends("Alice", new List<string> { "Bob" });
var clone = original with { Name = "Carol" };
clone.Friends.Add("Dave");
Console.WriteLine(string.Join(", ", original.Friends)); // Bob, Dave!
// original 的 Friends 也被修改了!
// 解决:使用不可变集合
public record SafePerson(string Name, IReadOnlyList<string> Friends)
{
// with 表达式时需要显式处理引用类型
}优点
缺点
Record 与序列化
JSON 序列化中的 Record
/// <summary>
/// Record 与 System.Text.Json 的配合
/// </summary>
// Record 作为 JSON 请求体
public record CreateUserRequest(
string UserName,
string Email,
int Age);
// System.Text.Json 序列化
var json = JsonSerializer.Serialize(new CreateUserRequest("Alice", "alice@test.com", 28));
// {"UserName":"Alice","Email":"alice@test.com","Age":28}
// 反序列化
var request = JsonSerializer.Deserialize<CreateUserRequest>(json)!;
// ⚠️ init 属性的注意事项
// System.Text.Json 默认使用参数化构造函数
// 对于 record,优先使用主构造函数
// 自定义 JSON 序列化
public record Product
{
public int Id { get; init; }
public string Name { get; init; } = "";
public decimal Price { get; init; }
// JsonPropertyName 映射
[JsonPropertyName("product_name")]
public string DisplayName { get; init; } = "";
// JsonIgnore 排除
[JsonIgnore]
public string InternalNote { get; init; } = "";
}
// Newtonsoft.Json 注意事项
// 需要设置 JsonSerializerSettings
// settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
// record 的集合属性
public record OrderResponse(
int OrderId,
List<OrderItemResponse> Items,
decimal TotalAmount)
{
// 集合属性需要初始化,避免 null
public List<OrderItemResponse> Items { get; init; } = new();
}Record 与 EF Core
/// <summary>
/// Record 与 Entity Framework Core 的配合
/// </summary>
// EF Core 5+ 支持 record 作为实体类型
// 但需要注意:EF Core 需要无参构造函数
// ❌ record 作为实体(没有无参构造函数)
// public record User(string Name, string Email);
// ✅ 方式 1:添加无参构造函数
public record User
{
public int Id { get; init; }
public string Name { get; init; } = "";
public string Email { get; init; } = "";
// EF Core 需要的无参构造函数
public User() { }
}
// ✅ 方式 2:使用 owned 类型(值对象)
// record 作为 Owned Entity(值对象)
public class Order
{
public int Id { get; set; }
public ShippingAddress ShippingAddress { get; set; } = null!;
}
// record 作为值对象
public record ShippingAddress(string Street, string City, string ZipCode);
// DbContext 配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
entity.OwnsOne(o => o.ShippingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(200);
address.Property(a => a.City).HasMaxLength(100);
});
});
}
// ⚠️ record struct 在 EF Core 中的限制
// EF Core 对 record struct 的支持有限
// 推荐使用 record class 作为实体,record struct 作为 DTO总结
Record 是 C# 中定义数据类型的最佳方式。用于 DTO、事件、配置、值对象等场景时,比 class 更简洁、更安全。实体类(需要可变性和跟踪变化)仍应使用 class。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
- 框架与语言特性类主题要同时理解运行方式和工程组织方式。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
- 明确项目入口、配置管理、依赖管理、日志和测试策略。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
- 把 notebook 或脚本风格直接带入长期维护项目。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
- 继续补齐部署、打包、监控和性能调优能力。
适用场景
- 当你准备把《Record 类型与不可变数据》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《Record 类型与不可变数据》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Record 类型与不可变数据》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Record 类型与不可变数据》最大的收益和代价分别是什么?
