很多 C# 开发者第一次接触 record,会把它理解成“语法更短的 class”。但它真正改变的并不只是声明方式,而是对象相等性、复制方式和数据模型的设计习惯。本文按“它是什么、解决什么问题、什么时候该用”的顺序展开,结合 C# 9 与 C# 10+ 的能力,把 record、record struct 以及与 class/struct 的差异一次讲清。
record 到底是什么
record 是 C# 9 引入的新型引用类型(Reference Type),主要面向数据导向(Data-Oriented)的对象建模。它特别适合不可变的数据传输对象(DTO)、值对象,以及强调“内容是否相同”而不是“是不是同一个实例”的领域模型。
它的几个核心特征可以先记住:
- 内置值相等性:两个
record实例只要属性值相同,就会被视为相等。 - 语法更紧凑:可通过主构造函数直接声明属性。
- 天然适合不可变设计:通常与
init搭配使用。 - 对模式匹配友好:
with、解构和模式匹配配合自然。
基本语法:为什么它比 class 更像“数据模型”
简单声明
public record Person(string FirstName, string LastName);
这段代码里,record 关键字定义的是一个不可变引用类型,而 (string FirstName, string LastName) 不只是构造参数,也会生成对应的自动 init 属性。
它大致等价于下面这种传统写法:
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(string firstName, string lastName)
=> (FirstName, LastName) = (firstName, lastName);
// 自动生成值相等比较、Deconstruct 方法等
}
基础使用方式如下:
var p1 = new Person("Alice", "Smith");
Console.WriteLine(p1.FirstName); // Alice
如果你的对象本质上只是“携带一组数据”,这种写法通常比手动写属性、构造函数和比较逻辑更直接。
不可变对象怎么写:init-only 与 with 的配合
init-only 为什么适合 record
record 最常见的使用方式就是不可变设计。属性通常采用 init-only,也就是只能在初始化阶段赋值,实例创建后不能再随意改动。
public record Person(string FirstName, string LastName, int Age);
// 创建 record 实例
var person = new Person("John", "Doe", 30);
// 编译错误:不能修改属性
// person.Age = 31;
这种约束的价值在于:对象状态更稳定,传递时更容易推断,也更适合并发和函数式风格的数据处理。
with 表达式如何做非破坏性复制
不可变对象不能原地修改,因此 record 提供了 with 表达式来复制对象并修改局部字段。
var p1 = new Person("Alice", "Smith");
var p2 = p1 with { LastName = "Johnson" };
Console.WriteLine(p2.FirstName); // Alice
Console.WriteLine(p2.LastName); // Johnson
p1保持不变。p2是一个新实例。
这正是不可变对象最常见的更新方式:保留原值,生成新值,避免隐式副作用。
record 最关键的变化:值相等而不是引用相等
class、struct、record 的默认相等性区别
| 类型 | 相等比较(默认) |
|---|---|
class | 引用相等(Reference Equality) |
struct | 值相等(字段逐一比较) |
record | 值相等(自动生成 Equals/==) |
这意味着 record 虽然默认仍是引用类型,但比较行为更像“值对象”。

var p1 = new Person("Alice", "Smith");
var p2 = new Person("Alice", "Smith");
Console.WriteLine(p1 == p2); // True
Console.WriteLine(p1.Equals(p2)); // True
即使 p1 和 p2 是不同实例,只要属性值相同,它们就相等。对于 DTO、消息体、查询结果这类对象,这种行为通常比引用相等更符合直觉。
编译器会为 record 自动生成什么
编译器会为 record 自动补齐一批围绕值语义设计的成员:
Equals(object?)和GetHashCode():基于属性值计算。==和!=运算符。Deconstruct方法。ToString():输出形如Person { FirstName = 具体值, LastName = 具体值 }。
这也是为什么 record 很适合作为 DTO、查询结果和配置数据模型:常见样板代码几乎都由编译器接管了。
解构、模式匹配与继承:record 的配套能力
解构可以直接使用
编译器会自动生成 Deconstruct 方法,因此位置记录可以直接按元组风格拆开。
var person = new Person("Alice", "Smith");
var (first, last) = person;
Console.WriteLine($"{first} {last}");
同样的方式也适用于其他位置记录:
public record Order(int Id, decimal Price);
var order = new Order(1001, 99.99m);
var (id, price) = order; // 自动解构
为什么它特别适合模式匹配
record 非常适合和属性模式、类型模式一起使用:
if (person is Person { FirstName: "Alice" })
Console.WriteLine("Found Alice!");
如果再结合继承层次,它会更接近代数数据类型的写法。
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;
public static double CalculateArea(Shape shape)
{
return shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentException("Unknown shape")
};
}
// 使用模式匹配
var circle = new Circle(5);
var rectangle = new Rectangle(4, 6);
Console.WriteLine(CalculateArea(circle)); // 78.53981633974483
Console.WriteLine(CalculateArea(rectangle)); // 24
record 的继承与多态要注意什么
record 支持继承,而且会保留值相等语义:
public record Person(string Name);
public record Student(string Name, int Grade) : Person(Name);
Person p = new Student("Alice", 5);
Console.WriteLine(p is Student); // True
不过它的 Equals 会带上类型检查,因此父类和子类即使属性值相同,也不会因为“值相同”就被判定为相等。这个规则很重要,因为它能避免继承体系下出现语义混乱的比较结果。
C# 10+ 的 record struct:把值语义带到值类型
C# 10 引入了 record struct,它把 record 的值相等语义和 struct 的值类型特性结合到一起。
// Record struct(值类型)
public record struct Point(int X, int Y);
// 使用 record struct
var point1 = new Point(3, 4);
var point2 = new Point(3, 4);
Console.WriteLine(point1 == point2); // True
Console.WriteLine(point1); // Point { X = 3, Y = 4 }
// 修改 record struct(因为是值类型,所以可以修改)
point1.X = 5;
Console.WriteLine(point1); // Point { X = 5, Y = 4 }
// 只读 record struct
public readonly record struct ImmutablePoint(int X, int Y);
var immutablePoint = new ImmutablePoint(3, 4);
// immutablePoint.X = 5; // 编译错误:不能修改只读字段
这里要区分两个点:
record默认是引用类型。record struct是值类型,可变与否取决于你是否使用readonly等约束。
怎么在 class、struct、record、record struct 之间做选择
| 特性 | class | struct | record(class) | record struct |
|---|---|---|---|---|
| 类型 | 引用类型 | 值类型 | 引用类型 | 值类型 |
| 相等性 | 引用相等 | 值相等 | 值相等 | 值相等 |
| 默认不可变 | ❌(可变) | ❌(可变) | ✅(推荐 init) | ✅(推荐 readonly) |
| 内存分配 | 堆 | 栈/堆 | 堆 | 栈/堆 |
| 继承 | 支持 | 仅接口 | 支持 | 仅接口 |
with 表达式 | ❌ | ❌ | ✅ | ✅ |
不使用主构造函数也可以写 record
如果你不想用位置记录,也可以像普通 class 一样显式声明属性:
public record Car
{
public string Brand { get; init; }
public string Model { get; init; }
}
这种写法的关键区别不在语法,而在于它仍然保留了 record 自动生成的值相等比较能力。
实际应用场景:哪些模型最适合用 record
DTO 与 API 数据模型
对于 API 请求、响应、查询结果这类“主要负责携带数据”的对象,record 往往比传统 class 更贴切。
// API 响应 DTO
public record ApiResponse<T>(bool Success, string Message, T Data);
// API 请求 DTO
public record CreateUserRequest(string Username, string Email, string Password);
// 使用 record DTO
public class UserController : ControllerBase
{
[HttpPost]
public IActionResult CreateUser(CreateUserRequest request)
{
// 处理请求...
var response = new ApiResponse<User>(true, "User created successfully", user);
return Ok(response);
}
}
这类模型通常不强调身份,而强调“字段内容是否一致”,因此与值语义天然契合。
配置对象
配置对象也是 record 的常见落点,因为配置天然更偏向初始化后只读。
// 应用程序配置
public record AppSettings
{
public string ConnectionString { get; init; }
public int MaxRetryAttempts { get; init; } = 3;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
public LoggingSettings Logging { get; init; }
}
public record LoggingSettings
{
public string Level { get; init; } = "Information";
public string FilePath { get; init; } = "logs/app.log";
}
// 使用配置
var settings = new AppSettings
{
ConnectionString = "Server=localhost;Database=MyDb",
MaxRetryAttempts = 5,
Logging = new LoggingSettings { Level = "Debug" }
};
推荐与不推荐的边界
推荐使用 record 的场景:
- 不可变的数据模型:
DTO、API响应对象。 - 配置项、设置类。
- 数据库查询结果(
EF Core中很常见)。 - 事件/消息模型(
Event Sourcing、CQRS)。
不推荐使用 record 的场景:
- 对象需要频繁修改。
- 需要严格的引用语义,例如缓存中的唯一实例。
性能与使用注意事项
record 并不意味着零成本,实际使用时有几件事要提前判断:
record默认仍是引用类型(除非是record struct),因此仍会在堆上分配,存在GC开销。- 值比较会带来额外的
Equals/GetHashCode计算成本。 with表达式会创建新对象,在高频更新场景中不宜滥用。
简单说,record 优先解决的是建模表达力和语义正确性,而不是无条件追求运行时性能最优。
总结:把 record 用在“按值看待对象”的地方
| 特性 | 说明 |
|---|---|
| 核心目标 | 简化不可变数据类型的声明和比较 |
| 类型 | 引用类型(默认)/值类型(record struct) |
| 相等性 | 自动实现基于值的相等性(Equals、==) |
| 不可变性 | 推荐使用 init 或 readonly |
| 复制 | with 表达式实现非破坏性复制 |
| 解构 | 自动生成 Deconstruct,支持元组解构 |
| 典型应用 | DTO、API 数据模型、配置对象、事件/消息对象 |
如果一个对象更像“数据本身”,你通常应该优先考虑 record;如果它更强调身份、生命周期或可变行为,传统 class 往往仍然更合适。把这个判断标准抓住,record 的使用边界就会清晰很多。









