记录类型
record类型与with表达式
概述
记录类型(record)是 C# 9 引入的一种引用类型,专门用于表示不可变的数据模型。它自动为你生成值相等性比较、不可变修改(with 表达式)和格式化输出等功能,大幅减少了定义数据类时的样板代码。C# 10 又引入了 record struct,将记录类型的便利带到值类型。
为什么需要记录类型?传统的 class 默认使用引用相等性(两个对象是否是同一个实例),而数据模型通常需要值相等性(两个对象的内容是否相同)。记录类型自动实现了值相等性,还提供了 with 表达式来创建修改了部分属性的新实例,非常适合函数式编程风格。
基础概念
值相等性:两个 record 实例的所有属性值都相同时,它们被视为相等。这与 class 的引用相等性不同。
不可变性:record 的属性默认是只读的(init-only),创建后不能修改。需要修改时使用 with 表达式创建新实例。
with 表达式:基于现有 record 创建一个新实例,只修改指定的属性,其余属性保持不变。
位置记录:使用简洁的构造函数语法定义 record,编译器自动生成属性、构造函数和 Deconstruct 方法。
record 与 record struct:record 是引用类型(堆分配),record struct 是值类型(栈分配)。选择取决于是否需要引用语义。
快速上手
最简单的记录类型:
// 位置记录:一行代码定义一个完整的数据类型
public record Person(string Name, int Age);
// 创建实例
var p1 = new Person("Alice", 25);
var p2 = new Person("Alice", 25);
// 值相等性:内容相同即相等
Console.WriteLine(p1 == p2); // True(class 的话会是 False)
Console.WriteLine(p1.Equals(p2)); // True
// with 表达式:创建修改了部分属性的新实例
var p3 = p1 with { Age = 26 };
Console.WriteLine(p3); // Person { Name = Alice, Age = 26 }
// 原实例不受影响
Console.WriteLine(p1.Age); // 25(不可变)
详细用法
位置记录
位置记录自动生成构造函数、属性和 Deconstruct 方法:
// 定义位置记录
public record Point(double X, double Y);
// 自动生成的功能:
// 1. 构造函数:new Point(1.0, 2.0)
// 2. 只读属性:X, Y
// 3. Deconstruct 方法:var (x, y) = point;
// 4. 值相等性:point1 == point2
// 5. ToString:Point { X = 1, Y = 2 }
var p = new Point(3.0, 4.0);
// 使用 Deconstruct 解构
var (x, y) = p;
Console.WriteLine($"X={x}, Y={y}");
// with 表达式
var p2 = p with { X = 5.0 };
Console.WriteLine(p2); // Point { X = 5, Y = 4 }
添加额外成员
记录类型可以像普通类一样添加额外成员:
public record Student(string Name, int Age, double Score)
{
// 添加额外属性
public string Grade { get; init; } = CalculateGrade(Score);
// 添加方法
public string GetInfo() => $"{Name},{Age}岁,成绩{Score},等级{Grade}";
// 添加静态方法
private static string CalculateGrade(double score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
}
var student = new Student("张三", 20, 85.5);
Console.WriteLine(student.GetInfo()); // 张三,20岁,成绩85.5,等级B
record struct
record struct 是值类型的记录,适用于小型数据结构:
// 值类型记录
public record struct Point(double X, double Y);
// 引用类型记录
public record PointRef(double X, double Y);
// 区别:
// record struct 是值类型,存储在栈上,赋值时复制
// record 是引用类型,存储在堆上,赋值时共享引用
var s1 = new Point(1, 2);
var s2 = s1; // 值复制
s2.X = 10; // 修改 s2 不影响 s1
Console.WriteLine(s1.X); // 1
var r1 = new PointRef(1, 2);
var r2 = r1; // 引用复制(但 record 是不可变的,所以实际效果类似)
record struct 的属性默认是可变的(与 record 不同):
// record struct 默认属性可变
public record struct MutablePoint(double X, double Y);
var mp = new MutablePoint(1, 2);
mp.X = 10; // 允许修改
// 如果需要不可变,使用 init
public record struct ImmutablePoint(double X, double Y)
{
public double X { get; init; } = X;
public double Y { get; init; } = Y;
}
自定义构造函数和验证
public record EmailAddress(string Address)
{
// 验证构造函数参数
public string Address { get; init; } =
string.IsNullOrWhiteSpace(Address)
? throw new ArgumentException("邮箱地址不能为空")
: Address;
// 添加计算属性
public string Domain => Address.Split('@').LastOrDefault() ?? "";
// 自定义验证逻辑
public bool IsValid => Address.Contains('@') && Address.Contains('.');
}
var email = new EmailAddress("user@example.com");
Console.WriteLine(email.Domain); // example.com
Console.WriteLine(email.IsValid); // True
// 以下会抛出异常
// var invalid = new EmailAddress("");
继承
记录类型支持继承:
// 基类记录
public record Animal(string Name, string Type);
// 派生记录
public record Dog(string Name, string Breed) : Animal(Name, "Dog");
public record Cat(string Name, bool IsIndoor) : Animal(Name, "Cat");
var dog = new Dog("旺财", "柴犬");
var cat = new Cat("小花", true);
Console.WriteLine(dog); // Dog { Name = 旺财, Type = Dog, Breed = 柴犬 }
Console.WriteLine(cat); // Cat { Name = 小花, Type = Cat, IsIndoor = True }
// 值相等性考虑继承层次
Animal a1 = new Dog("旺财", "柴犬");
Animal a2 = new Dog("旺财", "柴犬");
Console.WriteLine(a1 == a2); // True
Animal a3 = new Cat("旺财", true);
Console.WriteLine(a1 == a3); // False(类型不同)
模式匹配与记录
记录类型与模式匹配配合良好:
public record Order(int Id, decimal Amount, string Status);
var order = new Order(1, 99.9m, "Shipped");
// 属性模式匹配
var message = order switch
{
Order { Status: "Pending", Amount: < 100 } => "小额待处理订单",
Order { Status: "Pending" } => "待处理订单",
Order { Status: "Shipped" } => "已发货订单",
Order { Status: "Delivered", Amount: > 1000 } => "大额已送达订单",
_ => "其他订单"
};
// 解构模式
if (order is Order(_, var amount, "Shipped"))
{
Console.WriteLine($"已发货,金额: {amount}");
}
集合中的记录
public record Product(string Name, decimal Price);
var products = new List<Product>
{
new("笔记本电脑", 5999),
new("手机", 3999),
new("耳机", 299)
};
// 值相等性在集合操作中很有用
var target = new Product("手机", 3999);
var found = products.Contains(target); // True
// 使用 Distinct 去重
var duplicates = new List<Product>
{
new("手机", 3999),
new("手机", 3999), // 与上一条值相等
new("手机", 4999) // 价格不同
};
var unique = duplicates.Distinct().ToList(); // 只剩2个
// 使用 with 批量修改
var discounted = products.Select(p => p with { Price = p.Price * 0.9m }).ToList();
常见场景
API 响应模型
// 定义 API 响应的记录类型
public record ApiResponse<T>(T Data, string Message, bool Success);
public record UserDto(int Id, string Name, string Email);
// 使用
var response = new ApiResponse<UserDto>(
new UserDto(1, "张三", "zhang@example.com"),
"获取成功",
true
);
// 序列化为 JSON
var json = System.Text.Json.JsonSerializer.Serialize(response);
领域事件
// 用记录类型定义领域事件
public record DomainEvent(DateTime OccurredAt)
{
public Guid EventId { get; init; } = Guid.NewGuid();
}
public record UserCreated(Guid UserId, string Name) : DomainEvent(DateTime.UtcNow);
public record UserDeleted(Guid UserId, string Reason) : DomainEvent(DateTime.UtcNow);
public record OrderPlaced(Guid OrderId, decimal Amount) : DomainEvent(DateTime.UtcNow);
// 事件处理
void HandleEvent(DomainEvent e)
{
switch (e)
{
case UserCreated created:
Console.WriteLine($"用户创建: {created.Name}");
break;
case UserDeleted deleted:
Console.WriteLine($"用户删除: {deleted.Reason}");
break;
case OrderPlaced placed:
Console.WriteLine($"下单: {placed.Amount:C}");
break;
}
}
注意事项
record 的不可变是浅层的:with 表达式创建新实例时,引用类型的属性仍然指向同一个对象。如果 record 包含可变的引用类型属性,需要注意这一点。
record 不能从 class 继承:record 只能从 record 继承,不能从普通 class 继承。但 record 可以实现接口。
ToString 可能泄露敏感信息:record 的 ToString 会输出所有属性值,包含敏感数据的 record 应重写 ToString 或使用 [DebuggerDisplay] 控制。
序列化兼容性:record 的主构造函数参数与属性名需要匹配,否则某些序列化器可能无法正确反序列化。
进阶用法
自定义相等性
// 重写相等性逻辑
public record Money(decimal Amount, string Currency)
{
// 自定义相等性比较:金额和货币都相同才相等
public virtual bool Equals(Money? other)
{
if (other is null) return false;
// 允许微小误差
return Math.Abs(Amount - other.Amount) < 0.01m
&& Currency == other.Currency;
}
// 必须同时重写 GetHashCode
public override int GetHashCode() => HashCode.Combine(
Math.Round(Amount, 2), Currency);
}
使用 record 实现建造者模式
public record QueryOptions(
int Page = 1,
int PageSize = 20,
string SortBy = "Id",
bool SortDescending = false,
string? SearchTerm = null)
{
// 链式构建
public QueryOptions WithPage(int page) => this with { Page = page };
public QueryOptions WithSearch(string term) => this with { SearchTerm = term };
public QueryOptions WithSort(string field, bool desc = false) =>
this with { SortBy = field, SortDescending = desc };
}
// 使用
var options = new QueryOptions()
.WithPage(2)
.WithSearch("关键词")
.WithSort("Name", desc: true);