C# record

2023-10-24 13:57:05 浏览数 (2)

1.概要

C# record 是一种引用类型,是C# 9.0引入的新特性。它是一种轻量级的、不可变的数据类型,具有只读属性,因此在创建后无法更改,这使得它线程安全。与类不同,record 类型是基于值相等而不是唯一标识符的,这意味着两个 record 实例只要它们的属性相等,就被视为相等。

Records 在数据传输、模式匹配和不可变性方面非常有用。它们提供了更简洁的语法,用于创建不可变的数据对象,避免了手动实现不可变性所需的样板代码。

1.1特性

  1. 不可变性: record是不可变的,一旦创建,其状态无法修改,确保了线程安全和数据一致性。
  2. 值相等性: record实例的相等性是基于其属性的值,而不是引用。两个record对象只要它们的属性值相等,就被视为相等。
  3. 简化的语法: record提供了一种简化的语法来定义数据类型,避免了手动实现不可变性的繁琐代码。
  4. 模式匹配: record在模式匹配中非常有用,可以轻松地与模式匹配语法结合使用,简化了对数据结构的操作。
  5. 记录的层次结构: 你可以构建具有层次结构的record,这在某些情况下比使用类更为方便。
  6. 可读性: 由于其简洁的语法,record使得代码更易读,尤其是在处理大量数据传输或需要比较相等性的场景中。

1.2不可变性

不可变性的关键在于以下几点:

  1. 只读属性: record的属性默认是只读的,即它们只能在构造函数中初始化,初始化完成后就不能再修改。这确保了属性值在对象创建后不可变。
  2. init 属性: C# 9.0引入了init关键字,用于声明属性的初始化器。被init修饰的属性只能在对象初始化期间被设置,之后将变为只读,实现了不可变性。
  3. 不可变性的优势: 不可变性使得对象在多线程环境中更安全,因为它们不能被并发修改。同时,不可变对象更易于缓存和重用,提高了性能。
  4. 记录初始化: 通过构造函数或者对象初始化器进行初始化时,record类型的属性将被赋予初始值。一旦初始化完成,属性值不能再被改变。
  5. 不可变性的实现: 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; }
}

虽然记录可以是可变的,但它们主要用于支持不可变的数据模型。 记录类型提供以下功能:

  • 用于创建具有不可变属性的引用类型的简明语法
  • 内置行为对于以数据为中心的引用类型非常有用:
    • 值相等性
    • 非破坏性变化的简明语法
    • 用于显示的内置格式设置
  • 支持继承层次结构

前面的示例展示了引用类型记录和值类型记录之间的一些区别:

  • recordrecord class 声明引用类型。 class 关键字是可选项,但可以为读者提高清晰度。 record struct 声明值类型。
  • 位置属性在 和 readonly record struct 中不可变。 它们在 中可变。

本文的其余部分将讨论 record classrecord struct 类型。 每个部分都详细说明了这些差异。 你应该在 record classrecord struct 之间作出决定,该过程类似于在 classstruct 之间作出决定。 record 一词用于描述应用于所有记录类型的行为。 record structrecord 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语句进行模式匹配,根据记录类型的属性值进行不同的操作。例如:

代码语言:javascript复制
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语句中解构记录类型,直接获取属性的值,使得代码更加简洁:

代码语言:javascript复制
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关键字添加额外的条件,使得模式匹配更加灵活:

代码语言:javascript复制
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);

在上述例子中,FirstNameLastName属性是只读的,它们的值只能在对象初始化的时候被设置,之后不能再被修改,确保了对象的不可变性和线程安全性。

使用场景

  1. 不可变性要求高: 当你需要创建不可变类型,即对象一旦创建就不能被修改时,可以使用 Record。这确保了对象的线程安全性,并且在分布式系统中特别有用。
  2. 简化数据传递: 如果你需要频繁地传递一些数据,但这些数据在传递过程中不应该被修改,Record 提供了一种简单、直观的方式来表示这些数据。
  3. 模式匹配: 在需要使用模式匹配进行数据处理的情况下,Record 类型可以提供更加清晰和简洁的模式匹配语法,用于处理各种数据情况。
  4. API 返回值: 当你需要设计 API,返回的数据对象应该是不可变的,以确保客户端无法修改这些数据,Record 是一个理想的选择。
  5. 数据传输对象(DTO): 在分布式系统中,常常需要传输数据对象,而这些对象通常是不可变的。使用 Record 类型可以简化 DTO 的定义和处理。
  6. 替代只读结构体: 如果你需要创建只读的数据结构体,Record 提供了更加简洁的语法,避免了传统只读结构体的冗长代码。

缺点

  1. 性能开销: Record 类型通常需要更多的内存,因为它们包含了用于比较对象的元数据。
  2. 不可变性带来的限制: Record 类型是不可变的,一旦创建就不能修改。这种不可变性可能在某些场景下引入不便,特别是需要频繁更新对象状态的情况。
  3. 相等性比较的复杂性: Record 类型的相等性比较是基于其属性的,而不是引用。这种方式可能在一些复杂的数据模型中引入比较的复杂性。
  4. 对象实例增多: Record 类型的不可变性意味着每次修改都会生成一个新的对象实例,这可能导致内存中存在大量的对象实例。

这些缺点并不意味着 Record 类型不可用,而是在选择使用 Record 类型时需要权衡其优势和缺点,以便选择最适合特定场景的数据模型。

0 人点赞