1.概要
C# record 是一种引用类型,是C# 9.0引入的新特性。它是一种轻量级的、不可变的数据类型,具有只读属性,因此在创建后无法更改,这使得它线程安全。与类不同,record 类型是基于值相等而不是唯一标识符的,这意味着两个 record 实例只要它们的属性相等,就被视为相等。
Records 在数据传输、模式匹配和不可变性方面非常有用。它们提供了更简洁的语法,用于创建不可变的数据对象,避免了手动实现不可变性所需的样板代码。
1.1特性
- 不可变性:
record
是不可变的,一旦创建,其状态无法修改,确保了线程安全和数据一致性。 - 值相等性:
record
实例的相等性是基于其属性的值,而不是引用。两个record
对象只要它们的属性值相等,就被视为相等。 - 简化的语法:
record
提供了一种简化的语法来定义数据类型,避免了手动实现不可变性的繁琐代码。 - 模式匹配:
record
在模式匹配中非常有用,可以轻松地与模式匹配语法结合使用,简化了对数据结构的操作。 - 记录的层次结构: 你可以构建具有层次结构的
record
,这在某些情况下比使用类更为方便。 - 可读性: 由于其简洁的语法,
record
使得代码更易读,尤其是在处理大量数据传输或需要比较相等性的场景中。
1.2不可变性
不可变性的关键在于以下几点:
- 只读属性:
record
的属性默认是只读的,即它们只能在构造函数中初始化,初始化完成后就不能再修改。这确保了属性值在对象创建后不可变。 - init 属性: C# 9.0引入了
init
关键字,用于声明属性的初始化器。被init
修饰的属性只能在对象初始化期间被设置,之后将变为只读,实现了不可变性。 - 不可变性的优势: 不可变性使得对象在多线程环境中更安全,因为它们不能被并发修改。同时,不可变对象更易于缓存和重用,提高了性能。
- 记录初始化: 通过构造函数或者对象初始化器进行初始化时,
record
类型的属性将被赋予初始值。一旦初始化完成,属性值不能再被改变。 - 不可变性的实现:
record
类型的不可变性由编译器自动生成的代码保证,确保了对象的状态不会被修改。
init关键字:
- 通过使用
record
定义数据模型,结合init
关键字,可以确保对象的属性在初始化后不能再被修改。
示例:
代码语言:javascript复制public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
// 使用record和init创建不可变对象
var person = new Person("John", "Doe");
// 以下操作是不允许的,因为属性是只读的(init关键字的作用)
// person.FirstName = "Jane"; // 编译错误
此外,还可以创建具有可变属性和字段的记录:
代码语言:javascript复制public record Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
};
记录结构也可以是可变的,包括位置记录结构和没有位置参数的记录结构:
代码语言:javascript复制public record struct DataMeasurement(DateTime TakenAt, double Measurement);
代码语言:javascript复制public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
虽然记录可以是可变的,但它们主要用于支持不可变的数据模型。 记录类型提供以下功能:
- 用于创建具有不可变属性的引用类型的简明语法
- 内置行为对于以数据为中心的引用类型非常有用:
- 值相等性
- 非破坏性变化的简明语法
- 用于显示的内置格式设置
- 支持继承层次结构
前面的示例展示了引用类型记录和值类型记录之间的一些区别:
record
或record class
声明引用类型。class
关键字是可选项,但可以为读者提高清晰度。record struct
声明值类型。- 位置属性在 和
readonly record struct
中不可变。 它们在 中可变。
本文的其余部分将讨论 record class
和 record struct
类型。 每个部分都详细说明了这些差异。 你应该在 record class
和 record struct
之间作出决定,该过程类似于在 class
和 struct
之间作出决定。 record 一词用于描述应用于所有记录类型的行为。 record struct
或 record class
用于描述仅适用于 struct 或 class 类型的行为。 record
类型是在 C# 9 中推出的;record struct
类型是在 C# 10 中推出的。
1.3相等性
record
类型的相等性是基于值相等性(value equality)的,意味着当两个record
对象的所有属性值都相等时,它们被认为是相等的。这种相等性是自动生成的,包括Equals
、==
、!=
和GetHashCode
方法,它们默认会比较record
对象的所有属性值。
这意味着,只要两个record
对象的所有属性值相等,它们就被视为相等,即使它们不是同一个对象实例。
例如:
代码语言:javascript复制public record Person(string FirstName, string LastName);
var person1 = new Person("John", "Doe");
var person2 = new Person("John", "Doe");
bool isEqual = person1 == person2; // 返回true,因为属性值相等
record
类型的相等性使得比较对象更加直观和简便,因为你只需要关心对象的属性值是否相等,而不必担心对象实例的引用。
1.4模式匹配
模式匹配语法: 使用switch
语句进行模式匹配,根据记录类型的属性值进行不同的操作。例如:
var person = new Person("Alice", 30);
switch (person)
{
case Person p when p.Age > 18:
Console.WriteLine($"{p.Name} is an adult.");
break;
case Person p:
Console.WriteLine($"{p.Name} is a minor.");
break;
}
模式解构: 允许在switch
语句中解构记录类型,直接获取属性的值,使得代码更加简洁:
switch (person)
{
case Person { Name: "Alice", Age: 30 }:
Console.WriteLine("Alice is 30 years old.");
break;
}
嵌套模式匹配: 可以在记录模式中使用其他模式,实现更复杂的匹配逻辑:
代码语言:javascript复制switch (person)
{
case Person { Age: > 18, Name: var name }:
Console.WriteLine($"{name} is an adult.");
break;
}
模式匹配中的When条件: 可以使用when
关键字添加额外的条件,使得模式匹配更加灵活:
switch (person)
{
case Person { Age: > 18 } when person.Name.StartsWith("A"):
Console.WriteLine($"{person.Name} is an adult and starts with 'A'.");
break;
}
1.5 线程安全
Record类型是线程安全的。Record类型的线程安全性是通过其不可变性(immutability)来实现的。不可变性意味着一旦对象被创建,其状态就不能被修改。在Record类型中,属性是只读的,一旦对象被创建,这些属性的值就不能被修改,从而确保了对象的不可变性和线程安全性。因此,多个线程可以安全地访问和共享Record对象而无需担心数据被意外修改的问题。
例如,在C#中使用Record类型定义一个不可变的Person对象:
代码语言:javascript复制public record Person(string FirstName, string LastName);
在上述例子中,FirstName
和LastName
属性是只读的,它们的值只能在对象初始化的时候被设置,之后不能再被修改,确保了对象的不可变性和线程安全性。
使用场景
- 不可变性要求高: 当你需要创建不可变类型,即对象一旦创建就不能被修改时,可以使用 Record。这确保了对象的线程安全性,并且在分布式系统中特别有用。
- 简化数据传递: 如果你需要频繁地传递一些数据,但这些数据在传递过程中不应该被修改,Record 提供了一种简单、直观的方式来表示这些数据。
- 模式匹配: 在需要使用模式匹配进行数据处理的情况下,Record 类型可以提供更加清晰和简洁的模式匹配语法,用于处理各种数据情况。
- API 返回值: 当你需要设计 API,返回的数据对象应该是不可变的,以确保客户端无法修改这些数据,Record 是一个理想的选择。
- 数据传输对象(DTO): 在分布式系统中,常常需要传输数据对象,而这些对象通常是不可变的。使用 Record 类型可以简化 DTO 的定义和处理。
- 替代只读结构体: 如果你需要创建只读的数据结构体,Record 提供了更加简洁的语法,避免了传统只读结构体的冗长代码。
缺点
- 性能开销: Record 类型通常需要更多的内存,因为它们包含了用于比较对象的元数据。
- 不可变性带来的限制: Record 类型是不可变的,一旦创建就不能修改。这种不可变性可能在某些场景下引入不便,特别是需要频繁更新对象状态的情况。
- 相等性比较的复杂性: Record 类型的相等性比较是基于其属性的,而不是引用。这种方式可能在一些复杂的数据模型中引入比较的复杂性。
- 对象实例增多: Record 类型的不可变性意味着每次修改都会生成一个新的对象实例,这可能导致内存中存在大量的对象实例。
这些缺点并不意味着 Record 类型不可用,而是在选择使用 Record 类型时需要权衡其优势和缺点,以便选择最适合特定场景的数据模型。