前置知识: C#

记录类型

4 minIntermediate2026/6/14

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);