建立記錄類型

記錄 是使用 以值為基礎的相等類型。 C# 10 新增 記錄結構 ,讓您可以將記錄定義為實值型別。 如果記錄類型定義相同,則記錄類型的兩個變數相等,如果針對每個字段,則這兩筆記錄中的值都相等。 如果所參考的物件是相同的類別類型,而且變數參考相同的物件,則類別類型的兩個變數相等。 以值為基礎的相等表示您可能想要在記錄類型中的其他功能。 當您宣告 而非 recordclass時,編譯程式會產生許多這些成員。 編譯程式會針對 record struct 類型產生這些相同的方法。

在本教學課程中,您將了解如何:

  • 決定您是否將 record 修飾詞新增至 class 類型。
  • 宣告記錄類型和位置記錄類型。
  • 將方法取代為記錄中編譯程序產生的方法。

必要條件

您必須設定您的電腦以執行 .NET 6 或更新版本,包括 C# 10 或更新版本編譯程式。 從 Visual Studio 2022.NET 6 SDK 開始,即可使用 C# 10 編譯程式。

記錄的特性

您可以使用 關鍵詞宣告類型record、修改 classstruct 宣告,以定義記錄。 您可以選擇性地省略 class 關鍵字來建立 record class。 記錄會遵循以值為基礎的相等語意。 為了強制執行值語意,編譯程式會為您的記錄類型產生數種方法(適用於 record class 類型和 record struct 類型):

記錄也會提供的 Object.ToString()覆寫。 編譯程式會使用 Object.ToString()合成方法來顯示記錄。 當您撰寫本教學課程的程式代碼時,您將探索這些成員。 記錄支援 with 表達式,以啟用記錄的非破壞性突變。

您也可以使用更簡潔的語法來宣告 位置記錄 。 當您宣告位置記錄時,編譯程式會為您合成更多方法:

  • 主要建構函式,其參數符合記錄宣告上的位置參數。
  • 主要建構函式的每個參數的公用屬性。 這些屬性僅適用於record class類型和readonly record struct類型。 針對 record struct 類型,它們是 讀寫
  • Deconstruct從記錄擷取屬性的方法。

建置溫度數據

數據和統計數據是您想要使用記錄的案例之一。 在本教學課程中,您將建置可計算 不同用途度天數 的應用程式。 度日 是一段時間、周或月的熱度(或缺乏熱量)的量值。 度日會追蹤並預測能源使用量。 更熱的日子意味著更多的空調,更冷的日子意味著更多的爐子使用。 學位日有助於管理植物種群,並在季節變化時與植物生長相互關聯。 學位日有助於跟蹤動物遷徙,以符合氣候的物種。

公式是以指定日期的平均溫度和基準溫度為基礎。 若要計算一段時間內的度數,您需要一段時間的高溫和低溫。 讓我們從建立新的應用程序開始。 建立新的主控台應用程式。 在名為 「DailyTemperature.cs」 的新檔案中建立新的記錄類型:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

上述程式代碼會 定義位置記錄。 記錄 DailyTemperature 是 , readonly record struct因為您不打算繼承它,而且它應該是不可變的。 HighTempLowTemp 屬性只是 init 屬性,這表示它們可以在建構函式中設定,或使用屬性初始化表達式。 如果您要讓位置參數成為讀寫,您可以宣告 , record struct 而不是 readonly record struct。 此DailyTemperature類型也有主要建構函式,其具有兩個符合兩個屬性的參數。 您可以使用主要建構函式來初始化 DailyTemperature 記錄。 下列程式代碼會建立並初始化數筆 DailyTemperature 記錄。 第一個會使用具名參數來釐清 HighTempLowTemp。 其餘初始化表示式會使用位置參數來初始化 HighTempLowTemp

private static DailyTemperature[] data = [
    new DailyTemperature(HighTemp: 57, LowTemp: 30), 
    new DailyTemperature(60, 35),
    new DailyTemperature(63, 33),
    new DailyTemperature(68, 29),
    new DailyTemperature(72, 47),
    new DailyTemperature(75, 55),
    new DailyTemperature(77, 55),
    new DailyTemperature(72, 58),
    new DailyTemperature(70, 47),
    new DailyTemperature(77, 59),
    new DailyTemperature(85, 65),
    new DailyTemperature(87, 65),
    new DailyTemperature(85, 72),
    new DailyTemperature(83, 68),
    new DailyTemperature(77, 65),
    new DailyTemperature(72, 58),
    new DailyTemperature(77, 55),
    new DailyTemperature(76, 53),
    new DailyTemperature(80, 60),
    new DailyTemperature(85, 66) 
];

您可以將自己的屬性或方法新增至記錄,包括位置記錄。 您必須計算每天的平均溫度。 您可以將該屬性新增至 DailyTemperature 記錄:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
    public double Mean => (HighTemp + LowTemp) / 2.0;
}

讓我們確定您可以使用此數據。 將下列程式碼新增至 Main 方法:

foreach (var item in data)
    Console.WriteLine(item);

執行您的應用程式,您會看到類似下列顯示的輸出(已移除數個空間的數據列):

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }
DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }


DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }
DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

上述程式代碼會顯示編譯程式所合成之覆寫的 ToString 輸出。 如果您偏好不同的文字,您可以撰寫自己的 版本 ToString ,以防止編譯程式為您合成版本。

計算度天數

若要計算度日,您會從基準溫度和指定一天的平均溫度取得差異。 若要測量一段時間的熱量,您可以捨棄平均溫度低於基準的任何天數。 若要測量一段時間的冷度,您可以捨棄平均溫度高於基準的任何天數。 例如,美國使用 65F 作為加熱和冷卻度日的基礎。 這就是不需要加熱或冷卻的溫度。 如果一天有平均溫度為 70F,那天是五個冷卻度和零加熱度日。 相反地,如果平均溫度是 55F,那天是 10 度加熱日和 0 度冷卻日。

您可以將這些公式表示為記錄類型的小型階層:抽象度日類型和兩種用於取暖度日和冷卻度日的具體類型。 這些類型也可以是位置記錄。 它們會採用基準溫度和每日溫度記錄序列作為主要建構函式的自變數:

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}

抽象DegreeDays記錄是 和 CoolingDegreeDays 記錄的共用基類HeatingDegreeDays。 衍生記錄的主要建構函式宣告會顯示如何管理基底記錄初始化。 您的衍生記錄會宣告基底記錄主要建構函式中所有參數的參數。 基底記錄會宣告並初始化這些屬性。 衍生的記錄不會隱藏它們,但只會為其基底記錄中未宣告的參數建立和初始化屬性。 在此範例中,衍生的記錄不會新增新的主要建構函式參數。 將下列程式代碼新增至 方法,以測試您的 Main 程式代碼:

var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);

您將取得如下顯示的輸出:

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }
CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

定義編譯程式合成的方法

您的程式代碼會計算該時段內正確的加熱和冷卻度數。 但此範例示範為何您可能想要取代記錄的一些合成方法。 您可以在記錄類型中宣告自己的任何編譯程式合成方法版本,但複製方法除外。 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 和更新版本中,您可以將方法宣告 ToStringsealed 記錄類型。 這可防止衍生的記錄提供新的實作。 衍生的記錄仍會包含覆 PrintMembers 寫。 如果您不想顯示記錄的運行時間類型,您會密封 ToString 。 在上述範例中,您將失去記錄測量取暖或冷卻度天數的相關信息。

非破壞性突變

位置記錄類別中的合成成員不會修改記錄的狀態。 目標是您可以更輕鬆地建立不可變的記錄。 請記住,您會宣告 readonly record struct 來建立不可變的記錄結構。 請再次查看和CoolingDegreeDays的上述宣告HeatingDegreeDays。 新增的成員會在記錄的值上執行計算,但不會變動狀態。 位置記錄可讓您更輕鬆地建立不可變的參考類型。

建立不可變的參考型別表示您想要使用非破壞性的突變。 您可以使用表達式來建立類似現有記錄實例的新記錄實例。with 這些表達式是具有修改複本之其他工作分派的複製建構。 結果是新的記錄實例,其中每個屬性都已從現有記錄複製,並選擇性地修改。 原始記錄不變。

讓我們在示範 with 表達式的程式中新增幾個功能。 首先,讓我們建立一筆新記錄,以使用相同的數據計算成長度日。 成長度日 通常會使用 41F 作為基準,並測量高於基準的溫度。 若要使用相同的數據,您可以建立類似 coolingDegreeDays的新記錄,但具有不同的基底溫度:

// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

您可以將計算的度數與以較高基準溫度產生的數位進行比較。 請記住,記錄是 參考類型 ,而且這些復本是淺層複本。 不會複製數據的陣列,但兩筆記錄都會參考相同的數據。 這一事實在其他案例中是一個優勢。 對於成長的日數,追蹤前五天的總和很有用。 您可以使用表示式來建立具有不同源數據 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 類型。

您可以深入瞭解記錄類型的 C# 語言參考文章中的記錄,以及建議的記錄類型規格和記錄結構規格