定义编译器合成方法
代码将计算该时间段内正确的采暖和制冷度日数。 但此示例展示了为何需要替换记录的某些合成方法。 可以在记录类型中声明你自己的版本的任意编译器合成方法(clone 方法除外)。 clone 方法具有编译器生成的名称,你无法提供其他实现。 这些合成方法包括复制构造函数、System.IEquatable<T> 接口的成员、相等性和不相等测试以及 GetHashCode()。 为此,你需要合成 PrintMembers。 你还可以声明自己的 ToString,但 PrintMembers 为继承方案提供了更好的选择。 若要提供自己的合成方法版本,签名必须与合成方法相匹配。
控制台输出中的 TempRecords 元素不起作用。 它只显示类型。 可通过提供自己的合成 PrintMembers 方法的实现来更改此行为。 签名取决于应用于 record 声明的修饰符:
如果记录类型为 sealed 或 record struct,则签名为 private bool PrintMembers(StringBuilder builder);
如果记录类型不为 sealed 并派生自 object(即,它不声明基本记录),则签名为 protected virtual bool PrintMembers(StringBuilder builder);
如果记录类型不为 sealed 并派生自其他记录,则签名为 protected override bool PrintMembers(StringBuilder builder);
了解 PrintMembers 的目的之后,就可以轻松地理解这些规则。 PrintMembers 将记录类型中每个属性的相关信息添加到字符串。 该协定要求基本记录添加其要显示的成员,并假设派生成员将添加其成员。 每个记录类型都会合成一个 ToString 替代,与下面的 HeatingDegreeDays 示例类似:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("HeatingDegreeDays");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
在不打印集合类型的 DegreeDays 记录中声明 PrintMembers 方法:
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
return true;
}
签名声明一个 virtual protected 方法来匹配编译器的版本。 如果访问器出错,请不要担心;语言会强制执行正确的签名。 如果你忘记了任何合成方法的正确修饰符,则编译器会发出警告或错误,帮助你获取正确的签名。
在 C# 10 及更高版本中,可以将 ToString 方法声明为 sealed 记录类型。 这会阻止派生记录提供新的实现。 派生记录将仍包含 PrintMembers 替代。 如果不希望 ToString 显示记录的运行时类型,则可以将其密封。 在前面的示例中,你会丢失有关记录测量取暖度日数或降温度日数的位置信息。
非破坏性修改
位置记录类中的合成成员不会修改记录的状态。 目的是帮助你更轻松地创建不可变记录。 请记住,你声明了 readonly record struct 来创建不可变记录结构。 请再次查看前面的关于 HeatingDegreeDays 和 CoolingDegreeDays 的声明。 添加的成员对记录的值执行计算,但不会改变状态。 位置记录使你可以更轻松地创建不可变引用类型。
创建不可变的引用类型意味着需要使用非破坏性修改。 使用 with 表达式 创建与现有记录实例类似的新记录实例。 这些表达式是一个副本构造,其中包含修改副本的其他赋值。 结果是一个新的记录实例,其中每个属性都已从现有记录进行复制并选择性地进行了修改。 原始记录未发生更改。
让我们向程序添加一些演示 with 表达式的功能。 首先,创建一条新记录,使用相同数据计算增长的度日数。 增长的度日数通常使用 41F 作为基准,并测量超出基准的温度。 若要使用相同的数据,可创建一条类似于 coolingDegreeDays 的新记录,但基准温度不同:
// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);
可将计算得出的度数与在较高基准温度下生成的数字进行比较。 请记住,记录是引用类型,这些副本是浅表副本。 不会复制数据的数组,但两条记录都引用相同的数据。 在另一种场景中,这是一个优势。 对于温度增长的日数,记录前 5 天的总度数非常有用。 可以使用 with 表达式创建具有不同源数据的新记录。 下面的代码将生成这些累计数据的集合,然后显示这些值:
// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}
还可使用 with 表达式来创建记录的副本。 请勿指定 with 表达式的大括号之间的任何属性。 这意味着将创建一个副本,并且不会更改任何属性:
var growingDegreeDaysCopy = growingDegreeDays with { };
运行已完成的应用程序以查看结果。
总结
本教程介绍了记录的几个方面。 记录为基本用途是存储数据的类型提供了简洁的语法。 对于面向对象的类,基本用途是定义责任。 本教程重点介绍了位置记录,在这种记录中,你可以使用简洁的语法来声明记录的属性。 编译器会合成记录的多个成员,以复制和比较记录。 你可针对记录类型添加所需的任何其他成员。 在明确编译器生成的所有成员都不会改变状态的情况下,可以创建不可变的记录类型。 可借助 with 表达式轻松实现非破坏性修改。
记录提供了另一种定义类型的方法。 使用 class 定义来创建面向对象的层次结构,这些层次结构重点关注对象的责任和行为。 可为数据结构创建 struct 类型,这些数据结构可存储数据,并且足够小,以便进行有效复制。 当你需要基于值的相等性和比较、不需要复制值以及要使用引用变量时,可以创建 record 类型。 当希望某个类型的记录功能足够小,可以高效复制时,可以创建 record struct 类型。