1. 为什么要有 record
在 C#(还有整个 .NET)里,class(class)和 struct(struct)一直是主力。但它们各有短板。class 是引用类型、可变的,默认是按引用比较(极少数例外)。struct 是值类型(传递时会复制),默认按字节比较,而且大多数时候是可变的(直到有了 readonly struct)。
如果你想要一个简单、自然的数据记录,能方便地按值比较、快速克隆、又不用担心对象被谁改了——那就得自己造轮子,或者用 ValueTuple 甚至 System.Tuple 这种库。但这些都不够优雅、表达力也一般。
所以 C# 9 推出了 record —— 一种数据类型,声明超简洁,天生不可变,按值比较,专为数据而生。
2. 四种类型一览表
| class | struct | record | record struct | |
|---|---|---|---|---|
| 类别 | 引用类型 | 值类型 | 引用类型 | 值类型 |
| 可变性 | 默认可变 | 默认可变 | 默认不可变(init) | 默认不可变(init) |
| 比较方式 | 按引用(==) | 按值(字段) | 按值(fields/properties) | 按值(fields) |
| 克隆 | 只能手写 | 只能手写 | 自带支持(with) | 自带支持(with) |
| 继承 | 支持 | 不支持 | 支持 | 不支持 |
| 不可变性 | 要自己实现 | 要自己实现 | 实现超简单 | 实现超简单 |
| 语法 | 最啰嗦 | 简短 | 最短(位置型) | 挺短的 |
| 集合里用法 | 按引用 | 复制 | 按引用 | 复制 |
可视化图解
+----------------+ +----------------+ +--------------------+
| class | | struct | | record |
+----------------+ +----------------+ +--------------------+
| Reference Type | | Value Type | | Reference Type |
| Mutable | | Mutable | | Immutable (init) |
| == : Reference | | == : By Fields | | == : By Value |
+----------------+ +----------------+ +--------------------+
3. record、class 和 struct 的本质区别
内存表现:引用还是值?
- class 和 record —— 都是引用类型。传给函数时,复制的是对象的引用。
- struct 和 record struct —— 值类型。总是按字节复制(除非你显式按引用传)。
class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);
void Demo()
{
var pc = new PointClass { X = 1, Y = 2 };
var ps = new PointStruct { X = 1, Y = 2 };
var pr = new PointRecord(1, 2);
var prs = new PointRecordStruct(1, 2);
ChangeY(pc); // pc.Y 会变!
ChangeY(ps); // ps.Y 不会变——复制!
ChangeY(pr); // pr.Y 会变!
ChangeY(prs); // prs.Y 不会变——复制!
}
void ChangeY(dynamic p) { p.Y = 99; }
如果你觉得 dynamic 这里有点神奇——对,就是为了演示下,struct 传进去不会变,class 或 record(引用类型)会变。
比较:怎么判断两个对象相等?
- class:按引用比较(== 只有同一个内存对象才 true),除非你重写了 Equals。
- struct:默认按所有字段的值比较。
- record:按主构造器里所有字段/属性的值比较。
class Foo { public int A; public int B; }
record Bar(int A, int B);
var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);
Console.WriteLine(foo1 == foo2); // False! 不是同一个对象
Console.WriteLine(bar1 == bar2); // True! 值一样
有趣的点:
record struct 更牛:它们也是“按值”比较,像普通 struct,但有 record 的语法和特性。
4. 不变性:谁能保证不被改?
来对比下对象的安全性:
- class:默认很容易被改,除非你把所有字段都 readonly。
- struct:同理,但你可以声明成 readonly struct,这样字段和属性都不能改。
- record:一般字段都用 init 修饰,只能在构造器或对象初始化器(with)里赋值。这样数据传递更安全。
- record struct:同理,可以写成 readonly record struct,值类型也能享受 record 的不变性福利。
record Person(string Name, int Age);
var p1 = new Person("阿列克谢", 23);
// p1.Age = 24; // 报错!只能 init
var p2 = p1 with { Age = 24 }; // OK!创建新副本,数据更新
如果你项目很大,有一堆 entity 对象,record 能帮你避免很多“谁改了字段导致全挂”的 bug。
5. 语法:声明长啥样,怎么不搞混
// class
public class Product
{
public int Id { get; init; }
public string Name { get; init; }
}
// struct
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// record
public record Product(int Id, string Name);
// record struct
public record struct Point(int X, int Y);
你看,record 类型写法最短。构造器、析构器、相等判断、ToString() 等等全都“开箱即用”。
| 语法 | 自带功能 | 能继承吗 |
|---|---|---|
| class | 几乎啥都没有 | 能 |
| struct | 几乎啥都没有 | 不能 |
| record | ToString, Equals, Deconstruct, with | 能 |
| record struct | ToString, Equals, Deconstruct, with | 不能 |
6. 克隆和 with 操作符
只有 record 和 record struct 有专门的 with 操作符,可以轻松复制对象并改部分属性。
record User(string Name, int Age);
var user1 = new User("伊琳娜", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "伊琳娜", user2.Age == 29
class(class)的话只能手写复制方法,漏掉字段就等着踩坑吧。
7. 继承:谁能继承谁
- class:支持标准继承(类层级、虚方法、抽象啥的)。
- struct:不支持(只能实现接口)。
- record:支持继承,但有限制(只能 record 继承 record,不能和 class 混用)。
- record struct:不支持,和普通 struct 一样。
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // OK!
class Vehicle { }
class Car : Vehicle { } // OK!
// struct 不能继承 struct
继承细节下节课再聊 :P
8. 实际场景下怎么选类型
- class:大对象、行为多、生命周期长、状态会变、有层级(比如业务逻辑、UI 组件)。
- struct:小型值对象,追求极致复制速度、无 GC、最小开销(比如坐标、颜色、金额——适合快速复制)。
- record:DTO(数据传输对象)、参数对象、配置参数、不可变状态、计算结果,适合按内容比较。
- record struct:值类型,需要不可变和 record 行为,但又不想在堆上分配。
9. 常见错误和坑
有些程序员以为 record 就是“class 的替代品”。其实不是!如果你写的对象生命周期里状态会变——用 class。
如果你要按引用比较(比如单例模式、对象生命周期很重要)——用 class。
如果你做的是像数字或坐标点那样的值对象——用 struct 或 record struct。
如果你要不可变、易比较、经常在应用层间传递、存集合、日志、序列化的对象——选 record。
还要注意:如果你声明了 record class,属性全是只读,但里面有嵌套对象——只要嵌套字段本身是可变的,还是能被改。
record Student(string Name, List<int> Grades);
var s1 = new Student("安东", new List<int>() {5,5,5});
var s2 = s1 with { };
s2.Grades.Add(2); // 两个对象都“中招”了!s1.Grades == s2.Grades
GO TO FULL VERSION