C#基础(九):Record 类型

Record(记录类型)是 C# 9.0 引入的重大特性,C# 10.0 进一步补充了值类型版本,其核心定位是简化不可变数据模型的创建,原生支持值相等性、非破坏性修改与解构,专门面向「以数据为核心、行为较少」的场景,大幅减少数据载体类的样板代码。

Record 不是全新的类型分类,它本质是 classstruct 的语法糖,编译器会自动生成大量标准成员,让开发者专注于数据定义本身。

一、核心本质与设计思想

核心定义

Record 是一种内置值相等语义、支持非破坏性修改的特殊类型,分为两个分支:

  • record class(可简写为 record):引用类型记录,C# 9.0 引入,分配在托管堆
  • record struct:值类型记录,C# 10.0 引入,分配在栈(局部变量场景)

设计初衷

传统开发中,我们编写 DTO、VO、参数模型等纯数据类时,需要重复写大量样板代码:字段、属性、构造函数、EqualsGetHashCodeToString、克隆方法等。Record 的出现就是为了消除这些重复,让一行代码完成整个数据类型的定义。

核心定位

  • 主打数据承载,而非业务行为封装
  • 默认倾向不可变(特别是引用类型 record)
  • 原生支持值比较,而非引用比较
  • 配合模式匹配、解构语法,大幅提升数据处理代码的可读性

二、record class:引用类型记录

record class 是最常用的 record 形式,可简写为 record,本质是特殊的引用类型,编译器自动生成值相等等功能。

1. 基础定义方式

方式一:位置语法(最常用,主构造函数)

通过类名后的参数列表直接定义数据成员,编译器自动生成属性、构造函数、解构方法等全套成员。

// 一行定义一个不可变的学生数据类型
public record Student(string Name, int Age, string StudentId);

方式二:标准语法(手动定义属性)

和普通类写法一致,仅需加 record 关键字,适合需要自定义属性逻辑的场景。

public record Teacher
{
    public string Name { get; init; }
    public int Age { get; init; }
    public string Subject { get; init; }
}

两种方式可以混合使用:位置参数定义核心数据,类体内扩展额外属性和方法。

2. 核心特性详解

特性 1:默认不可变(init-only 属性)

位置语法生成的属性默认是 init 只读属性:对象初始化时可赋值,初始化完成后不可修改,保证数据的不可变性。

var stu = new Student("张三", 20, "2024001");
// stu.Name = "李四"; // 编译报错,初始化后不可修改

注意:不可变是引用类型 record 的默认行为,但并非强制 —— 你可以手动将属性改为 set 使其可变,但这违背了 record 的设计初衷,不推荐。

特性 2:值相等性

引用类型默认按引用比较,但 record class 重写了 EqualsGetHashCode==/!= 运算符,实现了按所有属性的值进行比较,只要两个对象的所有属性值都相同,就认为二者相等。

var s1 = new Student("张三", 20, "2024001");
var s2 = new Student("张三", 20, "2024001");

// 值比较:内容相同即相等
Console.WriteLine(s1 == s2);          // True
Console.WriteLine(s1.Equals(s2));     // True

// 引用不同:两个独立的堆对象
Console.WriteLine(object.ReferenceEquals(s1, s2)); // False

这是 record 和普通 class 最显著的区别 —— 普通 class 的 == 默认比较引用地址,record 默认比较数据内容。

特性 3:with 表达式:非破坏性修改

不可变对象不能直接修改属性,with 表达式提供了基于现有对象创建新对象,并修改部分属性的能力,全程不修改原对象,称为「非破坏性修改」。

var original = new Student("张三", 20, "2024001");

// 基于原对象创建新对象,只修改 Age 属性,其余属性复用
var updated = original with { Age = 21 };

Console.WriteLine(original.Age); // 20,原对象不变
Console.WriteLine(updated.Age);  // 21,新对象已修改
Console.WriteLine(updated.Name); // 张三,属性复用

底层原理:with 会调用编译器生成的「复制构造函数」,先完整拷贝原对象的所有属性,再修改指定属性,最终返回新对象。

注意:这是浅拷贝。如果属性是引用类型(如数组、集合),只会拷贝引用地址,新旧对象会共享同一个引用对象。

特性 4:自动解构

Record 自动生成 Deconstruct 方法,支持像元组一样解构为多个变量。

var stu = new Student("张三", 20, "2024001");

// 解构为独立变量
var (name, age, id) = stu;
Console.WriteLine($"姓名:{name},年龄:{age}");

// 也可以弃元不需要的字段
var (_, _, studentId) = stu;

解构顺序与位置参数的声明顺序完全一致。

特性 5:自动生成 ToString ()

编译器会自动重写 ToString() 方法,输出类型名和所有属性的键值对,非常适合调试。

Console.WriteLine(stu);
// 输出:Student { Name = 张三, Age = 20, StudentId = 2024001 }

3. 编译器自动生成的成员汇总

使用位置语法定义 record 时,编译器会自动生成以下成员,无需手动编写:

  1. 与位置参数对应的 init-only 公共属性
  2. 主构造函数(参数与位置参数一致)
  3. 复制构造函数(用于 with 表达式)
  4. 重写 Equals(object)EqualityContract 属性
  5. 重写 GetHashCode()
  6. 重载 ==!= 运算符
  7. Deconstruct 解构方法
  8. 重写 ToString() 方法

4. Record 的继承

Record class 支持继承,且遵循单继承规则:

  • record 可以继承另一个 record
  • record 不能继承普通类(除了 object
  • 普通类也不能继承 record

继承语法

子类主构造函数通过 base 关键字向父类传递参数。

// 父类记录
public record Person(string Name, int Age);

// 子类记录:继承 Person,扩展自己的属性
public record Employee(string Name, int Age, string Department, decimal Salary) 
    : Person(Name, Age);

继承场景下的相等性

相等比较时,只有两个对象的实际类型完全一致,且所有属性都相等,才会返回 true。父类引用指向子类对象时,不会因为父类属性相同就判定相等。

Person p1 = new Person("张三", 20);
Person p2 = new Employee("张三", 20, "研发部", 10000);

Console.WriteLine(p1 == p2); // False,类型不同

5. 手动扩展 Record

Record 不仅能存数据,也可以像普通类一样添加方法、计算属性、事件等成员。

public record Circle(double Radius)
{
    // 计算属性
    public double Area => Math.PI * Radius * Radius;
    
    // 方法
    public double GetCircumference() => 2 * Math.PI * Radius;
    
    // 静态成员
    public static Circle UnitCircle { get; } = new Circle(1);
}

三、record struct:值类型记录

C# 10.0 引入了 record struct,将 record 的值相等、with 语法、解构等能力带到了值类型世界,让轻量数据结构的定义更加简洁。

1. 基础定义

// 值类型记录:位置语法
public record Point(double X, double Y);

2. 核心注意点

默认是可变的(与 record class 相反)

这是最容易踩坑的区别:

  • record class 位置参数生成 init 只读属性,默认不可变
  • record struct 位置参数生成 get/set 可读写属性,默认可变

原因是值类型传统上默认可变,为了与普通 struct 的使用习惯保持一致,record struct 采用了可变默认设计。

不可变值类型记录:readonly record struct

如果需要不可变的值类型记录,使用 readonly record struct,此时属性会变为 init 只读,同时具备只读结构体的性能优势。

// 不可变值类型记录:推荐写法
public readonly record struct ReadonlyPoint(double X, double Y);

3. 与 record class 相同的能力

  • 支持 with 表达式非破坏性修改
  • 支持值相等比较
  • 支持解构
  • 自动生成友好的 ToString()
  • 可以实现接口

4. 与普通 struct 的区别

特性普通 structrecord struct
值相等 默认按值逐字段比较(ValueType.Equals,反射实现,性能低) 编译器生成强类型 Equals,性能更高
构造函数 C# 10 前不能自定义无参构造 位置语法自动生成构造函数
解构 需要手动实现 Deconstruct 自动生成解构方法
with 语法 不支持 原生支持
ToString 输出类型全名 输出属性键值对,适合调试

四、全方位对比:四种数据类型选型

对比维度普通 classrecord class普通 structrecord struct
类型分类 引用类型 引用类型 值类型 值类型
默认相等性 引用比较 值比较(所有属性) 值比较(反射) 值比较(强类型)
默认可变性 可变(set) 不可变(init) 可变 可变
内存分配 托管堆 托管堆 栈(局部变量) 栈(局部变量)
with 语法 不支持 支持 不支持 支持
自动解构 需手动实现 自动生成 需手动实现 自动生成
继承支持 支持单继承 仅支持继承 record 不支持继承 不支持继承
样板代码量 极少 中等 极少
适用场景 有复杂行为的对象 不可变数据模型、DTO 轻量值对象、高性能场景 轻量数据模型、需要值语义的结构体

五、常见坑点与注意事项

1. with 表达式是浅拷贝

with 只会逐属性拷贝值,如果属性是引用类型(数组、集合、类对象),只会复制引用地址,新旧对象会共享同一个引用对象,修改引用对象的内容会相互影响。

public record ClassRoom(string RoomId, List<string> Students);

var room1 = new ClassRoom("101", new List<string> { "张三", "李四" });
var room2 = room1 with { RoomId = "102" };

// 修改集合内容,两个 record 都会受影响
room2.Students.Add("王五");
Console.WriteLine(room1.Students.Count); // 3,共享同一个集合实例

解决方案:深拷贝场景需要手动创建新的引用对象,不可直接依赖 with

2. 可变 record 违背设计初衷

虽然语法上可以将属性改为 set,让 record 可变,但这会丧失不可变数据的所有优势(线程安全、可预测、易调试)。如果需要频繁修改状态,优先使用普通 class。

3. 继承场景的相等性陷阱

父类 record 引用指向子类实例时,相等比较会检查实际类型是否一致。不要因为两个对象的父类属性相同就认为它们相等。

4. Record 不是万能数据类

Record 适合纯数据载体,如果类型包含大量业务逻辑、复杂行为、状态变更,应该使用普通 class,不要强行用 record。

5. 不要依赖 ToString () 的输出格式

编译器生成的 ToString() 格式可能随版本变化,仅适合调试打印,不要作为业务逻辑的输入依据。

六、适用场景与最佳实践

推荐使用场景

  1. 数据传输对象(DTO/VO):API 请求 / 响应模型、前后端交互数据、跨层传递参数
  2. 不可变状态:配置项、事件参数、消息队列消息体
  3. 临时数据组合:方法返回多个值时,比元组语义更清晰
  4. 模式匹配:配合 switch 表达式做数据分发,可读性极佳
    string GetPersonInfo(Person person) => person switch
    {
        Employee e => $"员工{e.Name},部门:{e.Department}",
        Student s => $"学生{s.Name},学号:{s.StudentId}",
        _ => "未知人员"
    };
  5. 函数式编程风格:不可变数据 + 非破坏性修改,符合函数式编程思想

选型口诀

  • 需要继承、复杂行为 → 普通 class
  • 纯数据、不可变、引用类型 → record class
  • 轻量、高性能、值语义 → readonly record struct
  • 可变、小型值类型 → 普通 struct 或 record struct
posted @ 2026-07-23 10:44  挺秃然的i  阅读(4)  评论(0)    收藏  举报