1. 数据传递的问题
假设我们有个学校的应用,要在不同模块之间传递学生的信息:名字、出生年份、班级。一般咋搞?
我们通常会写个 class,像这样:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
看着挺正常。但这种写法有几个坑:
- 要比较两个学生是不是一样,默认只会比对象引用。也就是说,两个字段都一样但对象不同的学生——不相等!
- class 创建后还能被改,这有时候会出 bug(尤其是对象已经被别的地方用的时候);
- 一堆“模板”代码:构造函数、比较方法、复制(克隆)方法,ToString 等等。
你可能已经猜到了:C# 可以帮我们省掉这些麻烦!隆重介绍——record。
2. record 是啥?
record 是 C# 里专门为存数据设计的一种特殊类型。record 有两个核心特性:
- 不可变性(immutability): record 类型的对象默认是不可变的,也就是说属性值只能在创建时设定,之后不能改(准确说,set 是私有的)。当然你也可以声明可变的 record,但默认是不可变的。
- 值比较: 如果两个 record 对象所有字段的值都一样,它们就被认为是相等的(== 和 .Equals() 的行为不一样了!)。
其实 record 就是用来在应用层之间传递数据的神器(比如从数据库到 controller,从 controller 到 view 等等)。
3. record 的语法
最简单的写法——位置语法
如果只是想传一组值,可以用一行代码声明类型:
public record Student(string Name, int YearOfBirth, string Class);
底层发生了啥?编译器会自动帮你生成:
- 只读的自动属性(set 是私有的);
- 带所有参数的构造函数;
- 比较和复制方法;
- 超好用的 ToString,输出格式很友好!
怎么用位置 record
来,咱们在学校应用里用下这个新类型:
var student1 = new Student("伊万", 2008, "8A");
var student2 = new Student("玛丽亚", 2008, "8B");
访问属性和以前一样(只是不能改):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
试图创建后改属性
student1.Name = "彼得"; // 报错!属性是只读的。
如果你把上面那行取消注释,编译器立马就会报错:只读属性不能赋值。
自动生成的 ToString 长这样
Console.WriteLine(student1); // 输出:Student { Name = 伊万, YearOfBirth = 2008, Class = 8A }
不用手动格式化也很清楚!
4. record 对象的比较
提醒一下:如果用传统 class 创建两个内容一样的对象,它们还是不相等的:
var a = new Student("伊万", 2008, "8A");
var b = new Student("伊万", 2008, "8A");
Console.WriteLine(a == b); // 对于 class:false
但如果 Student 是 record,比较就和你想的一样了:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("伊万", 2008, "8A");
var b = new Student("伊万", 2008, "8A");
Console.WriteLine(a == b); // 对于 record:true!
也就是说,两个字段一样的 record 就算是内存里两个对象,也会被认为是相等的。
5. record 的底层实现
很多同学会惊讶,record 编译器帮我们做了多少事。来对比下,如果用 class 手写要写多少代码,record 又帮我们省了多少。
手写的传统 class
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; // 指向自己的 class
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
难怪程序员都快神经质了——同样的代码要写十遍!
record——一行搞定
public record Student(string Name, int YearOfBirth, string Class);
6. record 和不可变性:能干啥,不能干啥
record 默认属性只读,这能帮你避免一堆 bug。但如果你非要(比如对接很老的 API),也能声明可变的 record:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
现在这些字段可以改了,但你会失去一部分优势(比如安全性)。
7. record 的解构
因为位置语法很像 tuple,所以 record 也能很方便地解构:
var student = new Student("伊万", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // 伊万 - 2008, 8A
编译器会为每个位置 record 自动生成 Deconstruct 方法,这让你用 LINQ、switch 模式啥的都很爽。
8. record 是 class 还是 struct?
默认情况下 record 是引用类型,和 class 一样。也就是说,引用类型的所有特性(堆上存储、引用复制等)都适用。
如果你想要值类型(value type),C# 也有办法——可以写成这样:
public record struct Point(int X, int Y);
但传递数据时基本都用经典的 record,也就是引用类型。关于 record struct 的细节——下节课再聊 :P
class、struct、record 对比
| 类型 | 默认不可变 | 值比较 | 易解构 | 自动 ToString |
|---|---|---|---|---|
| class | 否 | 否(按引用) | 否 | 否 |
| struct | 否 | 是 | 否 | 否 |
| record | 是 | 是 | 是 | 是 |
9. 特点和常见错误
很多新手用 record 会搞混。有的人以为改了一个 record 对象的字段,另一个也会变(像 class 的引用复制)。不会! 还有,写 with 的时候,记住总是创建副本,不会改原对象。record 超适合需要数据纯净、可预测的业务逻辑。
对了,如果你用 init 而不是 set 声明字段,这些字段也只能在创建时或者用 with 赋值,之后就不能改了。
GO TO FULL VERSION