深入理解 C# 中的 Record 类型
深入理解 C# 中的 Record 类型_c# record-CSDN博客
深入理解 C# 中的 Record 类型
总目录
前言
在 C# 9.0 中引入了 record 关键字,用于定义记录类型(Record Type),这是一种新的引用类型,旨在简化不可变数据类型的定义与操作。记录类型具有许多独特的特性,如值相等性、简洁的语法和内置的不可变性支持,使其成为处理数据传输对象(DTO)、配置类以及其他需要不可变性的场景的理想选择。本文将详细介绍 C# 中记录类型的定义、特性和最佳实践。
一、什么是 Record 记录类型
1. 定义
记录类型 是一种特殊的引用类型,主要用于表示不可变的数据结构。与普通类不同,记录类型提供了默认的值相等性比较,并且支持简化的构造函数和解构模式。
2. 基础示例
假设我们有一个简单的 Person 记录类型:
public record Person(string Name, int Age);
- 1
 
在这个例子中,Person 是一个记录类型,它包含两个属性:Name 和 Age。记录类型会自动生成构造函数、解构方法和其他必要的成员。
二、为什么需要 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 } } }csharp 运行- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 
 - 
支持解构操作(可通过
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 } }csharp 运行- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 
 - 
自动实现
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
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 
Record 的
Equals和==运算符通过属性值比较对象是否相等,而非引用地址。
这点与Class不同,Class 需要重写Equals和重载==运算符 才能实现 自定义对象属性值的比较,而Record自动实现Equals、GetHashCode以及重载==运算符。
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(引用不相等)
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 
上例展示了 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);
     }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 
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 }
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 
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;
        }
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 
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 }
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 
2. 语法变体
1)record class(默认)
- 引用类型:
record class或简写record。 - 引用类型,但基于值语义比较
 
 public record Student(string Id, string Major);
- 1
 
2)record struct(C# 10+)
- 结构体类型:
record struct或readonly record struct。 - 值类型,适用于轻量级数据
 
public record struct Point(int X, int Y);
- 1
 
public readonly record struct Point(int X, int Y); // 不可变值类型。
- 1
 
值类型记录与引用类型记录的主要区别在于内存分配和复制行为。值类型记录在赋值时会进行深拷贝,而引用类型记录则共享引用。
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"; //初始化赋值后,更改会编译错误
 }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 
3. 高级用法
1)自定义 Record
可扩展 Record 以添加方法或重写默认行为:
public record Person(string FirstName, string LastName)
{
    public string FullName => $"{FirstName}{LastName}";
    public override string ToString() => $"Person: {FullName}";
}
- 1
 - 2
 - 3
 - 4
 - 5
 
public record Person
{
    public required string Name { get; init; }
    public string Greet() => $"Hello, {Name}!";
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 
2)可变 Record(不推荐)
通过 { get; set; } 定义可变属性,但违背 Record 设计初衷。
public record Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
- 1
 - 2
 - 3
 - 4
 - 5
 
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}
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 
4. 注意事项
- 若需要可变性,可显式声明 
set访问器,但会破坏值相等语义 - 避免在 
record中封装复杂业务逻辑,保持其数据容器的定位 - 深度嵌套对象时,
with表达式只执行浅拷贝 
四、适用场景
1. DTO(数据传输对象)
在分布式系统中,记录类型可以用于定义请求和响应的数据结构,简化 API 请求/响应模型的创建。
public record CustomerDto(int Id, string Name, string Email);
- 1
 
2. 配置对象
在应用程序配置中,记录类型可以用来存储和传递配置信息(确保配置加载后不可篡改)。如数据库连接字符串。
public record AppConfig(string ConnectionString, int MaxRetries);
- 1
 
3. 不可变数据模型
记录类型非常适合用于需要保持数据一致性和不可变性的场景。
 领域模型中的值对象(Value Object),确保数据不可变,避免副作用
4. 模式匹配
结合 switch 表达式实现高效数据匹配
var result = person switch
{
    { Age: >= 18 } => "Adult",
    _ => "Minor"
};
public string GetPersonInfo(Person p) => p switch
{
    { Age: < 18 } => "Minor",
    _ => "Adult"
};
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 
五、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);
- 1
 
编译器会自动生成构造函数、只读属性、基于值的相等性比较方法等。
2)传统类与 Record 的对比
传统类需要显式定义属性、构造函数和相等性方法,而 Record 将这些代码自动生成,减少样板代码量。例如:
// 传统类定义
public class Person
{
    public string FirstName { get; }
    public string LastName { get; }
    public Person(string firstName, string lastName) { ... }
    // 需手动实现 Equals、GetHashCode 等
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 
六、最佳实践
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" };
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 
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 } }
    }
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 
6. 版本控制
如果你的应用程序需要支持多个版本的 API,考虑为每个版本创建不同的记录类型。这样可以避免破坏现有客户端的兼容性。
public record PersonV1(string Name);
public record PersonV2(string Name, int Age);
- 1
 - 2
 
结语
回到目录页:C#/.NET 知识汇总
 希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
 Microsoft Docs: Records in C#
 Best Practices for Using Records in C#
 AutoMapper Documentation
					1181
					
				
					830
					
				
					6031
					
				
					341
					
				
					1577
					
				
					1090
					
				
					1172
					
				
					143
					
				
					236
					
				
					1597
					
				
					461
					
				
					641
					
				
热门文章
分类专栏
- 上一篇:
 - 深入理解 C# 中的主构造函数
 
                    
                
 
 
 
 
 
 
 
 
          
              
            
                      
                  
              
              
              
              
        
      
      
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


                
            
浙公网安备 33010602011771号
AkA_Mark: 第四小节中,在一个if判断里有个UserInfo的类是什么?
The Sheep 2023: 写的挺全的,终于不用忘了看破书了
朱小昊: 你的博客整体质量很高
husoftware: 这个插件比bindablebase好用。牛,佩服。
let_debug日记: 执行完还是回调到需要网络的installer状态