小窍门
开发软件的新手? 首先开始 学习入门 教程。 需要具有内置相等性的简洁数据类型时,会遇到记录。
是否在其他语言中有经验? C# 记录类似于 Kotlin 中的数据类或 Scala 中的事例类。 这些类型针对存储数据、编译器生成的相等性和 ToString复制语义进行优化。 对 C# 特定模式的 record class与record struct和with表达式部分进行浏览。
关键字 record 是一个修饰符,它可用于 class 或 struct。 它告诉编译器生成值相等性、格式化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;
}
单独编写 record 是 record class 的速记,而 record class 是一种引用类型。 编写 record struct 将创建值类型。 编译器从这两种情况下的位置参数生成属性,但默认值不同:
-
record class:属性为init-only(构造后不可变)。 -
record struct:属性默认为读写。 添加readonly(readonly record struct) 以将其设为仅限init。
如果需要更多控制,还可以编写具有标准属性语法的记录。 例如,若要使属性可读/可写而不是仅init:
public record Product
{
public required string Name { get; init; }
public decimal Price { get; set; }
}
record class 与 record 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:编译器生成Equals和GetHashCode方法和==/!=运算符,用于比较每个属性值。 具有相同数据的两个不同的对象相等。 -
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 class和record struct类型。 可以在分配、 foreach 循环和模式匹配中使用它。
记录继承
A record class 可以继承自另一个 record class。 记录无法从常规类继承,而类不能从记录继承:
public record Student(string FirstName, string LastName, int GradeLevel)
: Person(FirstName, LastName);
值相等性检查包括运行时类型,因此一 Person 个和一个 Student 具有相同 FirstName 值且 LastName 不被视为相等。 记录结构体不支持继承,因为结构体无法从其他类型继承。