总目录


前言

在 C# 9.0 中引入了 record 关键字,用于定义记录类型(Record Type),这是一种新的引用类型,旨在简化不可变数据类型的定义与操作。记录类型具有许多独特的特性,如值相等性、简洁的语法和内置的不可变性支持,使其成为处理数据传输对象(DTO)、配置类以及其他需要不可变性的场景的理想选择。本文将详细介绍 C# 中记录类型的定义、特性和最佳实践。


一、什么是 Record 记录类型

1. 定义

记录类型 是一种特殊的引用类型,主要用于表示不可变的数据结构。与普通类不同,记录类型提供了默认的值相等性比较,并且支持简化的构造函数和解构模式。

2. 基础示例

假设我们有一个简单的 Person 记录类型:

public record Person(string Name, int Age);

在这个例子中,Person 是一个记录类型,它包含两个属性:NameAge。记录类型会自动生成构造函数、解构方法和其他必要的成员。

二、为什么需要 Record?

1. 传统类的局限

传统类的局限性在数据建模中愈发明显:

  • 繁琐的样板代码:实现不可变类需手动定义只读属性、重写相等性方法
  • 线程安全隐患:可变状态在多线程环境中易引发竞态条件
  • 值语义缺失:类默认基于引用比较,数据相等性判断复杂

record应运而生,完美解决这些问题。它融合了引用类型的继承特性值类型的相等语义,成为不可变数据模型的理想载体。

2. Record 的优势

  • 简化代码record 提供了一种简洁的方式来定义数据传输对象,减少了样板代码。
  • 提高安全性record 的不可变性确保了数据在传输过程中的安全性。
  • 高可读性record 的明确字段名称和简洁语法使得代码更加易于理解和维护。

三、Record 详解

1. 核心特性

1)简洁的语法

记录类型提供了一种简洁的语法来定义不可变的数据结构。你只需要指定属性名称和类型,编译器会自动生成相应的构造函数、解构方法和属性访问器。

  • 编译器生成 ToString()(输出结构化的属性值)

    public record Person(string Name, int Age);
    
    class Program
    {
        static void Main()
        {
            var person = new Person("Alice", 30);
            Console.WriteLine(person);
            // 输出:Person { Name = Alice, Age = 30 }
        }
    }
    
  • 支持解构操作(可通过 Deconstruct 方法提取属性)

    public record Person(string Name, int Age);
    
    class Program
    {
        static void Main()
        {
            var person = new Person("Alice", 30);
    
            //(var name, var age) = person;
            var (name, age) = person; 	// 解构为元组
            Console.WriteLine(name);    // 输出:Alice
        }
    }
    
  • 自动实现init属性
    在这里插入图片描述

2)值相等性

record 类型支持值相等性,即两个 record 对象的内容相等时,它们被认为是相等的。

public record Person(string Name, int Age);

class Program
{
    static void Main()
    {
        var person1 = new Person("Alice", 30);
        var person2 = new Person("Alice", 30);

        Console.WriteLine(person1 == person2); // 输出: True
    }
}

Record 的 Equals== 运算符通过属性值比较对象是否相等,而非引用地址。
这点与Class不同,Class 需要重写 Equals 和重载 == 运算符 才能实现 自定义对象属性值的比较,而Record 自动实现EqualsGetHashCode 以及重载 == 运算符。

public record Person(string Name, int Age);
class Program
{
    static void Main()
    {
        var p1 = new Person("Alice", 30);
        var p2 = new Person("Alice", 30);
        var p3 = p2;
        var p4 = new Person("Jack",30);
        Console.WriteLine(p1 == p2); // 输出 True(值相等)
        Console.WriteLine(p1 == p3); // 输出 True(值相等)
        Console.WriteLine(p1 == p4); // 输出 False(值不相等)

        Console.WriteLine(p1.Equals(p2));// 输出 True(值相等)
        Console.WriteLine(p1.Equals(p3));// 输出 True(值相等)
        Console.WriteLine(p1.Equals(p4));// 输出 False(值不相等)

        Console.WriteLine(object.ReferenceEquals(p1,p2));// 输出 False(引用不相等)
        Console.WriteLine(object.ReferenceEquals(p1,p3));// 输出 False(引用不相等)
        Console.WriteLine(object.ReferenceEquals(p1,p4));// 输出 False(引用不相等)

    }
}

上例展示了 record 类型 分别使用 Equals 方法和 == 运算符以及ReferenceEquals方法进行比较的区别。

3)不可变性(默认行为)

Record 类型默认属性为只读(get; init;),创建后不可修改,确保线程安全。

public record Person(string Name, int Age);

class Program
{
     static void Main()
     {
         var person = new Person("Alice", 30);
         //person.Name = "";//由于不可变性,当给其赋值时,编译错误
         Console.WriteLine(person.Name);
     }
}

4)with 表达式

基于现有实例创建新对象(类似函数式编程的"复制-修改"), Record 默认不可变,属性只能在初始化时赋值。若需修改,需通过 with 表达式创建新副本:

public record Person(string Name, int Age);

class Program
{
    static void Main()
    {
        var person1 = new Person("Alice", 30);
        var person2 = person1 with { Age = 31 }; // 生成新对象,保留其他属性。

        Console.WriteLine(person1); // 输出: Person { Name = Alice, Age = 30 }
        Console.WriteLine(person2); // 输出: Person { Name = Alice, Age = 31 }
    }
}

5)模式匹配支持

结合 switch 实现基于属性的条件分支。

public record Person(string Name, int Age);

class Program
{
    static void Main()
    {
        var person = new Person("Alice", 30);

        switch (person)
        {
            case Person { Age: >= 18 } p: Console.WriteLine($"{p.Name} is Adult"); break;
            case Person { Age: < 18 and > 0 } p: Console.WriteLine($"{p.Name} is Minor"); break;
        }
    }
}

6)继承与派生

记录类型支持继承和派生,但有一些限制。子类必须也是记录类型,并且基类的参数必须在子类的构造函数中传递。

public record Person(string Name, int Age);
public record Employee(string Name, int Age, string Department) : Person(Name, Age);

class Program
{
    static void Main()
    {
        var employee = new Employee("Alice", 30, "Engineering");
        Console.WriteLine(employee); // 输出: Employee { Name = Alice, Age = 30, Department = Engineering }
    }
}

2. 语法变体

1)record class(默认)

  • 引用类型record class 或简写 record
  • 引用类型,但基于值语义比较
 public record Student(string Id, string Major);

2)record struct(C# 10+)

  • 结构体类型record structreadonly record struct
  • 值类型,适用于轻量级数据
public record struct Point(int X, int Y);
public readonly record struct Point(int X, int Y); // 不可变值类型。

值类型记录与引用类型记录的主要区别在于内存分配和复制行为。值类型记录在赋值时会进行深拷贝,而引用类型记录则共享引用。

3)属性自定义

可通过 init 访问器实现部分可变性(初始化后不可修改)。

public record Person
{
 public string Name { get; init; }
 public int Age { get; init; }
}

class Program
{
 static void Main()
 {
     var person = new Person() { Name = "Jack", Age = 15 };
     person.Name = "Nan"; //初始化赋值后,更改会编译错误
 }
}

3. 高级用法

1)自定义 Record

可扩展 Record 以添加方法或重写默认行为:

public record Person(string FirstName, string LastName)
{
    public string FullName => $"{FirstName} {LastName}";
    public override string ToString() => $"Person: {FullName}";
}
public record Person
{
    public required string Name { get; init; }

    public string Greet() => $"Hello, {Name}!";
}

2)可变 Record(不推荐)

通过 { get; set; } 定义可变属性,但违背 Record 设计初衷。

public record Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

3)特性标注


using System.Text.Json;
using System.Text.Json.Serialization;

public record Person(
    [property: JsonPropertyName("name")] string Name,
    [property: JsonPropertyName("age")] int Age
);


class Program
{
    static void Main()
    {
        var person = new Person("Jack",18);
        var json = JsonSerializer.Serialize(person);
        Console.WriteLine(json);//输出:{"name":"Jack","age":18}
    }
}

4. 注意事项

  • 若需要可变性,可显式声明 set 访问器,但会破坏值相等语义
  • 避免在 record 中封装复杂业务逻辑,保持其数据容器的定位
  • 深度嵌套对象时,with 表达式只执行浅拷贝

四、适用场景

1. DTO(数据传输对象)

在分布式系统中,记录类型可以用于定义请求和响应的数据结构,简化 API 请求/响应模型的创建。

public record CustomerDto(int Id, string Name, string Email);

2. 配置对象

在应用程序配置中,记录类型可以用来存储和传递配置信息(确保配置加载后不可篡改)。如数据库连接字符串。

public record AppConfig(string ConnectionString, int MaxRetries);

3. 不可变数据模型

记录类型非常适合用于需要保持数据一致性和不可变性的场景。
领域模型中的值对象(Value Object),确保数据不可变,避免副作用

4. 模式匹配

结合 switch 表达式实现高效数据匹配

var result = person switch
{
    { Age: >= 18 } => "Adult",
    _ => "Minor"
};

public string GetPersonInfo(Person p) => p switch
{
    { Age: < 18 } => "Minor",
    _ => "Adult"
};

五、Record 相关比较

1. Record 对比概览

特性 Record Class
不可变性 默认不可变 默认可变
相等性比较 基于属性值 基于引用地址
语法简洁性 一行代码定义完整类型 需手动编写属性和方法
复制与修改 支持 with 表达式 需手动实现克隆逻辑
继承 支持继承其他 Record 支持继承类或接口
ToString() 自动生成属性摘要 返回类型名称
操作 record class struct
内存分配 堆分配 栈分配
相等性比较 O(n)属性比较 内存逐字节比较
大对象传递 引用传递 值拷贝

优化建议:超过16字节的数据优先使用record class

特性 引用类型 record 结构体 record struct
值类型/引用类型 引用类型 值类型
属性可变性 默认不可变(init 可选) 默认不可变(init 可选)
继承支持 支持继承其他 record 不支持继承
内存占用 较高(堆分配) 较低(栈分配)

2. 示例对比

1)简洁语法

Record 可通过一行代码定义包含多个属性的不可变类型。例如,定义一个表示“人”的 Record:

public record Person(string FirstName, string LastName);

编译器会自动生成构造函数、只读属性、基于值的相等性比较方法等。

2)传统类与 Record 的对比

传统类需要显式定义属性、构造函数和相等性方法,而 Record 将这些代码自动生成,减少样板代码量。例如:

// 传统类定义
public class Person
{
    public string FirstName { get; }
    public string LastName { get; }
    public Person(string firstName, string lastName) { ... }
    // 需手动实现 Equals、GetHashCode 等
}

六、最佳实践

1. 保持简洁

记录类型应该保持简洁,专注于表示不可变的数据。避免在记录类型中添加复杂的业务逻辑或行为。

2. 使用场景

  • record 适用于需要传输数据但不需要复杂业务逻辑的场景,例如数据传输对象(DTO)。
  • 虽然 record 提供了便利,但在需要频繁修改对象状态的场景中,应避免使用。

3. 使用 init 访问器

如果你需要在对象初始化后允许某些属性被设置一次,可以使用 init 访问器。这样可以在保持不可变性的同时,允许有限的修改。

public record Person(string Name, int Age)
{
    public string Address { get; init; }
}

class Program
{
    static void Main()
    {
        var person = new Person("Alice", 30) { Address = "123 Main St" };
    }
}

4. 结合 record 和普通类

在需要实现接口或复杂逻辑时,可以将 record 与普通类结合使用。

5. 处理复杂映射

当你的记录类型包含嵌套对象时,确保正确处理这些嵌套对象的映射。你可以使用 AutoMapper 或其他映射库来简化这一过程。

public record Address(string Street, string City, string State);
public record Person(string Name, int Age, Address Address);

class Program
{
    static void Main()
    {
        var address = new Address("123 Main St", "New York", "NY");
        var person = new Person("Alice", 30, address);

        Console.WriteLine(person); // 输出: Person { Name = Alice, Age = 30, Address = Address { Street = 123 Main St, City = New York, State = NY } }
    }
}

6. 版本控制

如果你的应用程序需要支持多个版本的 API,考虑为每个版本创建不同的记录类型。这样可以避免破坏现有客户端的兼容性。

public record PersonV1(string Name);
public record PersonV2(string Name, int Age);

结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
Microsoft Docs: Records in C#
Best Practices for Using Records in C#
AutoMapper Documentation

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐