共用方式為


建立記錄類型

記錄 是使用 值型等號的類型。 您可以將記錄定義為參考型別或實值型別。 如果記錄類型定義相同,則記錄類型的兩個變數相等,如果針對每個字段,則這兩筆記錄中的值都相等。 如果所參考的物件是相同的類別類型,而且變數參考相同的物件,則類別類型的兩個變數相等。 基於值的相等性意指您可能希望的記錄類型中的其他功能。 當您宣告 record 而不是 class時,編譯程式會產生其中許多成員。 編譯程式會針對 record struct 類型產生這些相同的方法。

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

  • 決定您是否將 record 修飾詞新增至 class 類型。
  • 宣告記錄類型和位置記錄類型。
  • 在記錄中以您自己的方法取代編譯器生成的方法。

先決條件

記錄的特性

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

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

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

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

建置溫度數據

數據和統計數據是您想要使用記錄的案例之一。 在本教學課程中,您會建置應用程式,以針對不同的用途計算 度日度日數 是衡量若干天、幾周或幾個月內熱度(或缺乏熱度)的一個指標。 度日數會追蹤並預測能源使用量。 更熱的日子意味著更多的空調,更冷的日子意味著更多的爐子使用量。 積溫有助於管理植物群落,並在季節變化時幫助預測植物生長。 學位日有助於跟蹤動物遷徙,以符合氣候的物種。

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

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

此程式碼會定義一個 定位紀錄DailyTemperature 記錄是 readonly record struct,因為您不打算從它繼承,並且它應該是不可變的。 HighTempLowTemp 屬性只 init 屬性,這表示它們可以在建構函式中或使用屬性初始化表達式來設定。 如果您希望位置參數允許讀寫,您可以宣告 record struct,而不是 readonly record structDailyTemperature 類型也有 主要建構函式, 具有兩個符合兩個屬性的參數。 您可以使用主要建構函式來初始化 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 版本,以防止編譯程式為您合成版本。

計算度日

若要計算度日,您會從基準溫度和指定一天的平均溫度取得差異。 若要測量一段時間的熱量,您可以捨棄平均溫度低於基準的任何天數。 若要測量一段時間的冷度,您可以捨棄平均溫度高於基準的任何天數。 例如,美國使用華氏 65 度作為計算供暖和制冷度日的基準。 這就是不需要加熱或冷卻的溫度。 如果一天的平均溫度是華氏 70 度,那天是五個冷卻度日和零個取暖度日。 相反地,如果平均溫度是55 F,那天是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 記錄是 HeatingDegreeDaysCoolingDegreeDays 記錄的共用基類。 衍生記錄的主要建構函式宣告會顯示如何管理基底記錄初始化。 您的衍生記錄會宣告基底記錄主要建構函式中所有參數的參數。 基底記錄會宣告並初始化這些屬性。 衍生的記錄不會隱藏它們,但只會為其基底記錄中未宣告的參數建立並初始化屬性。 在此範例中,衍生的記錄不會新增新的主要建構函式參數。 將下列程式代碼新增至 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 宣告的修飾詞:

  • 如果記錄類型是 sealedrecord 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 方法,以符合編譯程式的版本。 如果您弄錯存取子,別擔心;語言會強制執行正確的簽章。 如果您忘記任何合成方法的正確修飾詞,編譯程式會發出警告或錯誤,以協助您取得正確的簽章。

您可以將 ToString 方法宣告為記錄類型中的 sealed。 這可防止衍生的記錄提供新的實作。 衍生的記錄仍會包含 PrintMembers 覆寫。 如果您不想顯示記錄的運行時間類型,請密封 ToString。 在前述範例中,您會失去關於記錄在哪裡測量取暖或降溫度日數的資訊。

非破壞性變異

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

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

讓我們將幾個功能新增至您的程式,以示範 with 表達式。 首先,讓我們建立一筆新記錄,以使用相同的數據計算成長度日。 成長度日 通常會使用 41 F 作為基準,並測量高於基準的溫度。 若要使用相同的數據,您可以建立類似 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# 語言參考文章中,深入瞭解 記錄類型、建議的記錄類型規格,以及 記錄結構規格