C# 记录类型

小窍门

开发软件的新手? 首先开始 学习入门 教程。 需要具有内置相等性的简洁数据类型时,会遇到记录。

是否在其他语言中有经验? C# 记录类似于 Kotlin 中的数据类或 Scala 中的事例类。 这些类型针对存储数据、编译器生成的相等性和 ToString复制语义进行优化。 对 C# 特定模式的 record classrecord structwith表达式部分进行浏览。

关键字 record 是一个修饰符,它可用于 classstruct。 它告诉编译器生成值相等性、格式化ToString,并通过with表达式实现非破坏性变异。 基础类型(类或结构)仍确定实例是使用引用还是值语义。 修饰器 record 在这些语义的基础上添加数据友好的行为。 当类型的主要角色存储数据时使用记录,应将具有相同值的两个实例视为相等。

何时使用记录

如果满足以下所有条件,请使用记录:

  • 类型的主要角色是存储数据。
  • 具有相同值的两个实例应相等。
  • 你需要不可变性(尤其是类型 record class )。
  • 你需要一个可读的ToString,而无需手动编写。

之间进行选择时。

  • 使用record class当您需要继承时,或者当类型足够大而使每次赋值进行复制会非常昂贵时。
  • 用于 record struct 小型自包含值,其中复制语义和堆栈分配非常有用。

避免在Entity Framework Core中为实体类型创建记录,因为这依赖于引用相等性来跟踪实体。 有关类型选项的更广泛比较,请参阅 “选择哪种类型”。

声明记录

可以将 record 应用于类或结构。 最简单的窗体使用 位置参数 来定义单个行中的构造函数和属性:

public record Person(string FirstName, string LastName);

相同的位置语法适用于 record struct 类型:

public record struct Coordinate(double Latitude, double Longitude);

public readonly record struct Temperature(double Celsius)
{
    public double Fahrenheit => Celsius * 9.0 / 5.0 + 32.0;
}

单独编写 recordrecord class 的速记,而 record class 是一种引用类型。 编写 record struct 将创建值类型。 编译器从这两种情况下的位置参数生成属性,但默认值不同:

  • record class:属性为 init-only(构造后不可变)。
  • record struct:属性默认为读写。 添加 readonlyreadonly record struct) 以将其设为仅限init

如果需要更多控制,还可以编写具有标准属性语法的记录。 例如,若要使属性可读/可写而不是仅init

public record Product
{
    public required string Name { get; init; }
    public decimal Price { get; set; }
}

record classrecord struct

record由于修饰符会保留基础类型的语义,record class因此在分配或比较引用时的行为和record struct行为不同。 record class 赋值时复制引用。 这两个变量都指向同一对象。 record struct分配数据会复制数据,因此对一个变量的更改不会影响另一个变量:

// Record class — assignment copies the reference
var p1 = new Person("Grace", "Hopper");
var p2 = p1; // p1 and p2 point to the same object:
Console.WriteLine(ReferenceEquals(p1, p2)); // True

// Record struct — assignment copies the data
var c1 = new Coordinate(47.6062, -122.3321);
var c2 = c1;
c2.Longitude = 0.0; // mutating c2 doesn't affect c1
Console.WriteLine(c1.Longitude); // -122.3321
Console.WriteLine(c2.Longitude); // 0

记录结构还提供编译器生成的值相等性:

var home = new Coordinate(47.6062, -122.3321);
var copy = home;

Console.WriteLine(home);           // Coordinate { Latitude = 47.6062, Longitude = -122.3321 }
Console.WriteLine(home == copy);   // True — value equality

选择何时需要继承,或者当实例足够大且复制成本高昂时进行选择 record class 。 选择 record struct 用于适合值类型复制语义的小型自包含数据。 有关值类型语义的详细信息,请参阅 结构

值相等性

record 修饰符为类和结构体提供编译器生成的逐属性的相等性。 下面是所有四种类型中的相等性工作原理:

  • 普通类:默认使用 引用相等性 。 运算符 == 检查两个变量是否指向同一对象,而不是数据是否匹配。
  • 纯结构:通过ValueType.Equals支持值相等性,但默认实现使用反射,这较慢且不生成==/!=运算符。
  • record class:编译器生成 EqualsGetHashCode 方法和 ==/!= 运算符,用于比较每个属性值。 具有相同数据的两个不同的对象相等。
  • record struct:与编译器生成的相等性相同 record class,但不使用反射,这使得它比纯结构相等性更快。

以下示例演示记录类相等性:

// Person is a record type with three properties: FirstName, LastName, and PhoneNumbers.
var phones = new string[] { "555-1234" };
var person1 = new Person("Grace", "Hopper", phones);
var person2 = new Person("Grace", "Hopper", phones);

Console.WriteLine(person1 == person2);              // True
Console.WriteLine(ReferenceEquals(person1, person2)); // False

person1.PhoneNumbers[0] = "555-9999";
Console.WriteLine(person2.PhoneNumbers[0]); // 555-9999 — same array

这两 Person 个实例是不同的对象,但它们是相等的,因为它们的所有属性值都匹配。 数组属性按引用而不是内容进行比较。 通过修改共享数组,在两个记录中都可以看到这些变化,因为数组本身并没有被复制。

使用 with 表达式进行非破坏性突变

记录通常是不可变的,因此创建后无法更改属性。 with表达式创建更改了一个或多个属性的副本,使原始记录保持不变。 此方法适用于两种 record class 类型和 record struct 类型:

var original = new Person("Grace", "Hopper");
var modified = original with { FirstName = "Margaret" };

Console.WriteLine(original); // Person { FirstName = Grace, LastName = Hopper }
Console.WriteLine(modified); // Person { FirstName = Margaret, LastName = Hopper }
Console.WriteLine(original == modified); // False

var copy = original with { };
Console.WriteLine(original == copy); // True

相同的语法适用于 record struct 类型:

var shifted = home with { Longitude = -122.0 };
Console.WriteLine(shifted);        // Coordinate { Latitude = 47.6062, Longitude = -122 }
Console.WriteLine(home == shifted); // False

表达式 with 复制现有实例,然后应用指定的属性更改。

位置记录和解构

位置记录生成一种 Deconstruct 方法,您可以使用该方法将属性值提取到各个变量中:

var (first, last) = person;
Console.WriteLine($"{first} {last}");
// Grace Hopper

解构可用于record classrecord struct类型。 可以在分配、 foreach 循环和模式匹配中使用它。

记录继承

A record class 可以继承自另一个 record class。 记录无法从常规类继承,而类不能从记录继承:

public record Student(string FirstName, string LastName, int GradeLevel)
    : Person(FirstName, LastName);

值相等性检查包括运行时类型,因此一 Person 个和一个 Student 具有相同 FirstName 值且 LastName 不被视为相等。 记录结构体不支持继承,因为结构体无法从其他类型继承。

另见