00:09:28
在 C# 开发中,选择合适的类型(record、class 或 struct)对代码的行为、性能和可维护性有深远影响。本文将从语法、行为和性能三个维度,深入分析如何做出明智的选择。
C# 继承了 C++ 和 Java 的类型系统精髓,其根本区别在于:class 是引用类型,而 struct 是值类型。这一区别影响了参数传递、内存分配和对象行为的方方面面。
当参数传递给方法时,C# 默认采用按值传递。对于引用类型,复制的是对象的引用;对于值类型,复制的则是整个对象本身。
值类型的复制机制导致了一个重要特性:即使将 struct 的属性声明为可变(mutable),修改操作也极易产生 bug。因为方法内部修改的是副本,而非原始对象。
这决定了值类型的设计应遵循不可变(immutable)原则,并采用写时复制(copy-on-write)技术。任何修改都应返回一个新实例,而非在原有实例上直接变更。
遗憾的是,C# 编译器不会警告未使用返回值的调用,这需要开发者自觉遵守不可变模式。
引用类型(class)则提供了更多灵活性。你可以选择将其设计为可变或不可变,这完全取决于你的业务场景和设计意图。
性能是选择类型时的重要考虑因素,但需要避免陷入误区。
包含少量(通常建议 1-2 个)字段的 struct 由于避免堆(heap)分配,性能表现极其出色。
当 struct 包含的字段增多(例如达到 3 个或更多),频繁复制整个实例的成本会急剧上升,性能可能反而不如引用类型。
最终的性能表现取决于实例被复制的次数和规模,需要根据具体场景进行权衡和测试。
使用泛型时,.NET 的 JIT 编译器会为不同大小的 struct 生成不同的底层实现代码,而为所有引用类型(引用大小固定)生成同一份实现。这是一个精巧的设计,开发者通常无需关心,但它解释了 .NET 泛型性能优于 Java 泛型的原因之一。
Record 类型的引入是为了更好地支持值语义(value semantics),即使其本身是引用类型。
将 class 声明为 record 后,编译器会自动为其生成:
with
表达式Equals
、GetHashCode
实现和相等运算符这使得 record 实例虽然存储在堆上,但行为上像一个值。
基于值的语义让 record 在特定场景下表现卓越:
with
表达式允许你仅指定需要修改的属性,其余属性自动复制到新实例。后续为 record 添加新属性,所有现有的 with
表达式无需修改仍可正常工作,极大地提升了代码的可维护性。C# 后续版本引入了 record struct
。其语法与 record class
相似,但本质是值类型。关键区别在于:
record struct
是可变的。若要强制不可变,需显式声明为 readonly record struct
。类型 | 核心特征 | 推荐使用场景 |
---|---|---|
Record Class | 引用类型,不可变,值语义 | DTO、不可变模型、哈希集合元素、字典键、任何需要值语义且数据量可能较大的场景 |
Class | 引用类型,可变/不可变灵活 | EF Core 实体、需要身份标识和可变状态的核心领域模型 |
Readonly Record Struct | 值类型,不可变,值语义 | 小型、轻量级的值对象(如包含不超过 2 个值类型组件的坐标、键值对等),对性能有极致要求 |
Record classes 已成为许多 immutable 设计的首选,特别是在序列化和 DTO 领域。
传统的 mutable classes 通常保留给需要身份标识和可变状态的实体(Entity),尤其是在与 Entity Framework Core 配合使用时。
Readonly record structs 则适用于非常小型的、不可变的值对象,它能提供最佳的性能表现。