解密 C# 中的记录类型:面向现代应用的不可变设计

解密 C# 中的记录类型:面向现代应用的不可变设计

C# 一直以来以其平衡的特性自豪——它本质上是面向对象的,但也稳步地融入了函数式编程的构造。随着 C# 9 引入记录类型(Record),并且在后续版本中(包括 C# 14)不断发展,开发者获得了一个强大的新工具,可以用最简洁的方式来建模不可变数据。但记录类型不仅仅是语法糖,它背后带来了对值语义、相等性和数据建模方式的深刻范式转变。

本文将带你深入了解 C# 中记录类型的使用,从其语义、功能到与传统类和结构体的区别。无论你是在处理 DTO(数据传输对象)、领域驱动设计(DDD)实体,还是构建函数式管道,记录类型都能提供一种简洁而表达力强的数据建模方式,简化并提升你的代码质量。

什么是记录类型?

从本质上讲,记录类型是一个引用类型,它采用基于值的相等语义。这意味着两个数据相同的记录实例被认为是相等的,而不管它们在内存中是否指向相同的引用。

传统上,C# 中的引用类型(类)默认使用引用相等。这意味着,只有当两个对象指向相同的内存地址时,它们才被认为是相等的。而记录类型改变了这一点,通过比较对象的值来进行相等性判断——这使得记录类型非常适合用于数据传输、状态建模以及表示不可变实体。

以下是一个最简单的示例:

public record User(string Name, int Age);

var u1 = new User("Alice", 30);
var u2 = new User("Alice", 30);

Console.WriteLine(u1 == u2); // True — 基于值的相等性

相比之下,如果 User 是一个类,比较结果会返回 False,因为它们是两个不同的引用。这种行为是有意为之,并且是一种强有力的转变,使 C# 更加贴近 F# 等函数式编程语言,在这些语言中,不可变性和结构相等性是默认的。

记录类型的分类:位置记录与名义记录

C# 支持两种主要的记录类型形式:

  1. 位置记录(Positional Records)
    位置记录使用简洁的构造函数语法直接在声明中定义。它们自动提供 Deconstruct()、ToString() 和 Equals() 等方法。
public record Product(string Name, decimal Price);

这种形式非常简洁,特别适用于数据驱动的应用程序,在这些应用中,重点是持有状态而非行为。它们非常适合用作 DTO、消息传递系统中的消息或跨层传递的值。

  1. 名义记录(Nominal Records,带显式属性)

你也可以声明带有手动定义属性的记录,这样可以让你更好地控制验证、格式化或自定义访问器等行为。

public record Order
{
    public string OrderId { get; init; }
    public DateTime Date { get; init; }
}

这种变体在需要更丰富的行为、注解或自定义数据模型时非常有用,例如在领域驱动设计(DDD)中的聚合根或视图模型中。

不可变性与 with 表达式

记录类型的一个最优雅的特性是通过 with 表达式进行无损的变更。你可以基于现有实例创建一个新实例,仅更改你指定的值,而不是修改原始实例。

var original = new User("Alice", 30);
var updated = original with { Age = 31 };

Console.WriteLine(original); // User { Name = Alice, Age = 30 }
Console.WriteLine(updated);  // User { Name = Alice, Age = 31 }

这种模式非常适合函数式编程、不可变状态管理和并发系统,因为共享的可变状态往往是 bug 的根源。with 表达式支持清晰的意图表达,使得编程风格更加简洁、安全。

值相等与引用相等

理解值相等(用于记录类型)与引用相等(用于类类型)之间的区别是避免意外行为的关键。例如:

public class Person(string Name);
public record Citizen(string Name);

var p1 = new Person("Bob");
var p2 = new Person("Bob");

var c1 = new Citizen("Bob");
var c2 = new Citizen("Bob");

Console.WriteLine(p1 == p2); // False
Console.WriteLine(c1 == c2); // True

这种区别不仅仅是理论性的,它影响到单元测试、字典查找、LINQ 查询和 UI 状态比较等方方面面。了解你的类型是通过结构还是通过身份进行比较,在大型代码库中至关重要。

继承与封闭性

记录类型支持继承,但默认情况下,记录类型是封闭的(sealed)。你可以使用 record class 或 record struct 语法创建记录类型层次结构,但重要的是要理解相等性比较是类型敏感的。

public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

Animal a = new Dog("Rex", "Labrador");
Animal b = new Dog("Rex", "Labrador");

Console.WriteLine(a == b); // True — 相同类型和相同值

当继承涉及到时,记录类型在多态和相等性方面引入了微妙的差异。基类记录无法轻易地覆盖派生类的行为,并且 with 表达式返回的是与原始记录相同的运行时类型——这样既保持了不可变性,也确保了类型安全。

记录类型在实际场景中的应用

以下是几个记录类型非常适用的实际场景:

  1. 建模不可变的领域事件
    在事件溯源系统中,每个状态变化都是一个独立的、不可变的事件。记录类型非常适合表示这些事件,既清晰又不可变。
public record OrderShipped(Guid OrderId, DateTime ShippedAt);
  1. 函数式管道
    当数据流经多个转换(例如,LINQ 或数据管道)时,你通常需要返回修改后的相同结构的版本。with 表达式能够优雅地实现这一点。
var transformed = inputData with { Status = "Processed" };
  1. 最小化 API 和数据契约
    ASP.NET Core 的最小 API 风格与记录类型非常契合,尤其在请求/响应类型中,能够减少冗余并提高表达性。

C# 14 中记录的新特性

C# 14 对记录的可用性进行了多项优化,特别是当与主构造函数和必需成员结合使用时。你现在可以:

  • 使用主构造函数,全面支持非记录类型,缩小记录与类之间的差距。
  • 将必需成员与记录声明混合使用,实现更安全的初始化。
  • 在组合基于记录的模型时依赖集合表达式和自然的 lambda 类型,尤其是在 API 中。

这些改进使得记录类型在 C# 中更加像一个一流的数据建模语言,它可以与 TypeScript 或 Kotlin 的简洁性相媲美,同时保持 C# 的强大鲁棒性。

结语:记录类型作为一种思维方式

理解记录类型不仅仅是学习一个语法特性——它更是改变我们设计和思考数据的方式。记录类型鼓励不可变性、清晰性和基于值的语义,这些都会带来更加可靠和可维护的代码。

无论你是在建模事件、构建 API 还是设计领域模型,记录类型提供了一种简洁而富有表现力的方式来封装状态,避免不必要的复杂性。它们不仅是类的替代品——它们代表了一种向更加声明式、意图明确的编程风格转变的根本性变化。

随着 C# 的不断发展,拥抱记录类型将帮助你编写出更短、更智能的代码。

注:转载文章,大家觉得上面文章如何?欢迎留言讨论。

本文使用chatgpt协助翻译。

作者:John Godel,版权归原作者John Godel所有

原文链接:c-sharpcorner.com/article/demystifying-records-in-c-sharp-immutable-design-for-modern-applications/

https://mp.weixin.qq.com/s?__biz=MzI2NDE1MDE1MQ==&mid=2650862550&idx=2&sn=858ec0f7327325c2d0347731a4ac9d75&poc_token=HBjlBWij7ispJs2VhF6ea60pbCr4oE7fDKieKkfc

Leave a Reply

Your email address will not be published. Required fields are marked *