CodeGym /课程 /C# SELF /认识 record 和位置语法

认识 record 和位置语法

C# SELF
第 19 级 , 课程 1
可用

1. 数据传递的问题

假设我们有个学校的应用,要在不同模块之间传递学生的信息:名字、出生年份、班级。一般咋搞?

我们通常会写个 class,像这样:


public class Student
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}
存学生数据的 class(老办法)

看着挺正常。但这种写法有几个坑:

  • 要比较两个学生是不是一样,默认只会比对象引用。也就是说,两个字段都一样但对象不同的学生——不相等!
  • class 创建后还能被改,这有时候会出 bug(尤其是对象已经被别的地方用的时候);
  • 一堆“模板”代码:构造函数、比较方法、复制(克隆)方法,ToString 等等。

你可能已经猜到了:C# 可以帮我们省掉这些麻烦!隆重介绍——record

2. record 是啥?

record 是 C# 里专门为存数据设计的一种特殊类型。record 有两个核心特性:

  1. 不可变性(immutability): record 类型的对象默认是不可变的,也就是说属性值只能在创建时设定,之后不能改(准确说,set 是私有的)。当然你也可以声明可变的 record,但默认是不可变的。
  2. 值比较: 如果两个 record 对象所有字段的值都一样,它们就被认为是相等的(==.Equals() 的行为不一样了!)。

其实 record 就是用来在应用层之间传递数据的神器(比如从数据库到 controller,从 controller 到 view 等等)。

3. record 的语法

最简单的写法——位置语法

如果只是想传一组值,可以用一行代码声明类型:


public record Student(string Name, int YearOfBirth, string Class);
record 的位置语法

底层发生了啥?编译器会自动帮你生成:

  • 只读的自动属性(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 对象的值比较

也就是说,两个字段一样的 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);
record——一行就全搞定!

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 的解构

编译器会为每个位置 record 自动生成 Deconstruct 方法,这让你用 LINQ、switch 模式啥的都很爽。

8. record 是 class 还是 struct?

默认情况下 record引用类型,和 class 一样。也就是说,引用类型的所有特性(堆上存储、引用复制等)都适用。

如果你想要值类型(value type),C# 也有办法——可以写成这样:


public record struct Point(int X, int Y);
record struct——值类型的 record

但传递数据时基本都用经典的 record,也就是引用类型。关于 record struct 的细节——下节课再聊 :P

classstructrecord 对比

类型 默认不可变 值比较 易解构 自动 ToString
class 否(按引用)
struct
record

9. 特点和常见错误

很多新手用 record 会搞混。有的人以为改了一个 record 对象的字段,另一个也会变(像 class 的引用复制)。不会! 还有,写 with 的时候,记住总是创建副本,不会改原对象。record 超适合需要数据纯净、可预测的业务逻辑。

对了,如果你用 init 而不是 set 声明字段,这些字段也只能在创建时或者用 with 赋值,之后就不能改了。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION