C#中 Class / Tuple / Record 的对比
·
文章目录
在 C# 的世界里,
record(记录类型)的出现是为了解决一个老生常谈的问题: 如何用最少的代码,定义一个纯粹用来存放数据的类型。
它本质上是一个特殊的 class(默认情况下),但编译器会自动为你生成大量关于“数据处理”的模板代码。
如果说 class 是为了行为(做事)设计的,那么 record 就是专门为了数据(存值)而生的。它结合了 class 的引用类型特性和元组的简便性,是目前 C# 定义“数据模型”的首选
C# 处理数据的三大法宝:ref/out(手段)、Tuple(轻量包)、Record(正式模型)
1. 核心语法
在没有 record 之前,你要写一个具备“值相等”特性的数据类,需要写几十行。现在只需要一行:
// 这一行叫做“位置参数”语法(Positional Records)
public record User(int Id, string Name, string Email);
// 这行代码自动生成了:属性、构造函数、解构函数、Equals、GetHashCode、ToString...
public record User(int Id, string Name, string Role);
// 使用
var user1 = new User(1, "老王", "Admin");
编译器在幕后偷偷帮你做了:
- 定义了
Id、Name、Email三个只读属性(init-only)。 - 生成了一个包含这三个参数的构造函数。
- 重写了
Equals、GetHashCode和==操作符(按值比较)。 - 生成了
ToString(),输出格式如User { Id = 1, Name = "..." }。 - 生成了
Deconstruct(解构)方法,支持var (id, name, email) = user;。
2. 深度特性:为什么它比普通 Class 强?
A. 值的相等性(Value Equality)
普通类比较的是“内存地址”,而 record 比较的是“属性值”。
var u1 = new User(1, "张三", "a@b.com");
var u2 = new User(1, "张三", "a@b.com");
Console.WriteLine(u1 == u2); // True (值相等)
Console.WriteLine(ReferenceEquals(u1, u2)); // False (地址不同)
B. 非破坏性修改(with 表达式)
由于 record 默认是不可变的(Immutable),你不能直接修改 u1.Name = "李四"。如果你想改名,你需要创建一个“克隆体”。
// 创建一个新对象,除了 Name 变了,其他都拷贝自 u1
var u3 = u1 with { Name = "李四" };
var person1 = new Person("Nancy", "Davolio");
var person2 = person1 with { FirstName = "John" };
C. 继承能力
record 支持继承,这是它比 struct 或 Tuple 强大的地方。
public record Manager(int Id, string Name, string Email, string Department)
: User(Id, Name, Email);
3. Record 的两种形态
虽然默认 record 是引用类型(record class),但 C# 10 引入了值类型版本:
- record class(或直接写 record):存放在堆上。适合大型数据模型、实体。
- record struct:存放在栈上。结合了
struct的性能和record的语法便利。注意:record struct默认是可变的(属性是set而不是init),除非你加readonly。
内存布局对比
| 类型声明 | 本质 | 存储位置 | 赋值行为 |
|---|---|---|---|
| record | 引用类型 (Reference Type) | 堆 (Heap) | 复制的是地址(指针)。两个变量指向同一个对象。 |
| record struct | 值类型 (Value Type) | 栈 (Stack) | 复制的是完整的数据内容。两个变量完全独立。 |
4. 选型指南:什么时候用什么?
| 需求场景 | 推荐方案 |
|---|---|
| 需要修改属性值、需要复杂的生命周期管理 | class |
| 仅仅是临时的多个返回值、不需要名字或名字不重要 | ValueTuple |
| 定义数据库实体、DTO、API 传输对象、且希望对象不可变 | record |
| 高性能、极小的数据包(如坐标点)、且希望按值比较 | record struct |
Record vs Class vs ValueTuple
| 特性 | Class | ValueTuple | Record |
|---|---|---|---|
| 类型属性 | 引用类型 (Heap) | 值类型 (Stack) | 引用类型 (Heap) |
| 相等性判断 | 引用相等(比地址) | 值相等(比内容) | 值相等(比内容) |
| 生命周期 | 长期存在,适合复杂业务 | 瞬时存在,适合局部传递 | 长期存在,适合作为数据模型 |
| 封装性 | 强,支持私有成员 | 无,字段全公开 | 强,支持复杂逻辑 |
| 概念 | 逻辑归属 | 理由 |
|---|---|---|
| Record | Types / Classes | 它定义了“是什么”。它是引用类型,具备面向对象的继承特性。 |
| Tuple | Types / Structs | 它也定义了“是什么”。虽然是轻量级的,但它在内存中是明确的结构体。 |
| ref / out | Methods / Parameters | 它们定义了“怎么传”。它们属于方法调用约定的一部分。 |
运行机制流程

术语表
- Init-Only Setter:一种属性访问器。它只允许在对象初始化的那一刻赋值(在构造函数或对象初始化器中),赋值后对象就成了“只读”状态。
- 非破坏性修改 (Non-destructive mutation):不改变原对象,而是通过复制原对象并修改指定部分来创建新对象。
- 解构 (Deconstruction):将对象拆解为多个变量的语法。
- 位置参数 (Positional Parameters):在类名后面括号里定义的参数,编译器会自动将其转换为对应的属性。
- 堆 (Heap):用于存储长寿对象。由 GC(垃圾回收器)负责清理。访问速度相对栈慢一点点(因为要根据指针去找地址)。
- 栈 (Stack):用于存储局部变量。随着方法调用结束自动释放,速度极快。
- 引用类型 (Reference Type):变量本身只存一个“内存地址”。
- 值类型 (Value Type):变量直接包含实际的数据。
更多推荐


所有评论(0)