记录是类似于类的引用类型,具有合成成员。它们支持从其他记录继承,但不支持从类继承。合成成员包括相等成员(如 和 )和复制成员(如复制构造函数)。
随着 C# 9 [1] 的发布,C# 引入了记录。但在较新的版本中,记录得到了改进,我们专注于 C# 12 中较新版本的记录,尽管这些变化很小,可以忽略不计。
Record 是一种特殊类型(记录类或记录**结构),**它被设计为具有内置相等性检查的不可变类型。
要声明记录,只需使用关键字而不是:recordclass
public record class MyRecord(string Name);
在上面的代码中,创建了一个基本的引用记录类。然而,在表面之下还有更多记录。当使用关键字创建类时,编译器会生成一个类,该类继承并有效地实现此接口。以下是我们记录的实施的一部分:recordIEquatable
[NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(MyRecord other)
{
if ((object) this == (object) other)
return true;
return (object) other != null && Type.op_Equality(this.EqualityContract, other.EqualityContract) && EqualityComparer<string>.Default.Equals(this.<Name>k__BackingField, other.<Name>k__BackingField);
}
如您所见,当使用该方法比较此类型的两个对象的相等性时,仅当所有属性的值相同或两个对象都引用同一对象时,它才会返回 true。这是期望的行为!Equals
除了与相等相关的函数外,编译器还会根据记录的内容生成一个隐藏方法,并重写以将记录表示为字符串,包括其公共成员。CloneGetHashCode()ToString()
编译器不允许您更改记录的属性(无论是记录类还是记录结构),但您可以借助 magic 关键字创建一个新对象,该关键字是 .让我们看一下下面的代码:with
var record1 = new MyRecord("Mohammad");
var record2 = record1 with { Name = "Mohammadreza" };
Console.WriteLine(record1.Equals(record2));
以及 C# 低级代码:
MyRecord record1 = new MyRecord("Mohammad");
MyRecord myRecord = record1.<Clone>$();
myRecord.Name = "Mohammadreza";
MyRecord record2 = myRecord;
Console.WriteLine(record1.Equals(record2));
当使用 with expression 更改记录时,编译器会创建另一个对象并将更改应用于新对象。这就是所谓的非破坏性突变!
值得一提的是,您可以通过记录免费获得可空性,对吧?一切都是不可变的,那么空值从何而来?差一点。不可变属性可以为 null,在这种情况下将始终为 null。[2]
还有另一种方法可以声明记录,就像声明一个类一样,并且有一个选项可以使记录的属性可变,但编译器生成的代码是相同的:
public record MyRecord
{
public string Name { get; set; }
}
用法:
var record1 = new MyRecord();
record1.Name = "Mohammad";
var record2 = record1 with { Name = "Mohammadreza" };
Console.WriteLine(record1.Equals(record2));
低级代码:
MyRecord record1 = new MyRecord();
record1.Name = "Mohammad";
MyRecord myRecord = record1.<Clone>$();
myRecord.Name = "Mohammadreza";
MyRecord record2 = myRecord;
Console.WriteLine(record1.Equals(record2));
在这种行为中,编译器使用对记录对象的任何更改生成一个新对象,可确保记录的不可变性。
值得注意的是,编译器为记录结构生成的代码与没有检查引用的记录类几乎相同。以及带有创建结构而不是类的编译器。另外,值得一提的是,record 类中的类是可选的,因此 record 类和 record 是相同的,但如果要创建记录结构,则必须编写 struct 关键字。
仅作为其属性组合而重要的对象。两个值对象的所有属性值相同,被视为相等。一个简单的小对象,如金钱或日期范围,其相等性不是基于身份的。[3]
在域驱动设计 (DDD) 中,值对象是一种对象类型,表示域中的特定属性或概念。它由其属性值定义,没有唯一标识符。值对象是不可变的,这意味着一旦设置,它们的值就无法更改。它们有助于捕获重要的领域概念,并使代码更具表现力和可理解性。
该定义接近 C# 中记录背后的想法,但是它错过了值对象的某些功能,如果您需要这些功能,我建议不要将记录用作值对象。
当涉及到包含对象列表的两个记录的相等性时,Record 不是一个好的选择,因为 .NET 将数组或列表视为引用可变类型。
var record1 = new MyRecord();
record1.Name = "Mohammad";
record1.Cars = new List<string> { "Benz", "BMW" };
var record2 = new MyRecord();
record2.Name = "Mohammad";
record2.Cars = new List<string> { "Benz", "BMW" };
Console.WriteLine(record1.Equals(record2));
结果是假的!
虽然 C# 中的记录默认设计为不可变,但它们仍允许可变属性。尽管使用 with 表达式是基于现有记录创建新记录的便捷方法,但它与值对象后跟的始终有效的模式相矛盾。使用 with 表达式时,无法验证新记录。
C# 中的记录基于所有属性自动生成相等和比较成员。但是,值对象应具有其相等的自定义实现。例如,在处理“Amount”等属性时,可能会出现 0.0000000001、0.00000001 和 0.001 等值为零的情况。此外,在某些情况下可能需要四舍五入。在记录中,这些值将被视为不同的值,但在值对象的上下文中,它们可能被视为相同。
虽然 C# 中的记录与域驱动设计中的值对象有相似之处,但在将记录用作值对象时存在某些限制和注意事项。记录可能无法正确处理数组的相等性,允许可变属性,或提供现成的自定义相等性实现。虽然记录非常适合 DTO(数据传输对象),但在某些情况下,需要对值对象进行更多自定义。在这种情况下,使用记录的好处可能不会超过对额外自定义的需求。