レコード型の作成

"レコード" は、"値ベースの等価性" を使用する型です。 C# 10 では、レコードを値の型として定義できるように "レコード構造体" が追加されています。 レコード型の 2 つの変数は、レコード型の定義が同じで、すべてのフィールドについて、両方のレコードの値が等しい場合、等しいと見なされます。 クラス型の 2 つの変数は、参照されているオブジェクトが同じクラス型であり、変数で同じオブジェクトが参照されている場合に等しくなります。 値ベースの等値性により、レコード型では他にもいくつかの機能が必要になると思われます。 class ではなく record を宣言すると、コンパイラによってそれらのメンバーの多くが生成されます。 コンパイラによって、record struct 型に対して同じメソッドが生成されます。

このチュートリアルで学習する内容は次のとおりです。

  • record 修飾子を class 型に追加するかどうかを決定します。
  • レコード型と位置指定レコード型を宣言する。
  • レコードのコンパイラによって生成されたメソッドを独自のメソッドに置き換える。

前提条件

C# 10 以降のコンパイラが含まれる .NET 6 以降が実行されるように、コンピューターを設定する必要があります。 C# 10 コンパイラは、Visual Studio 2022 以降または .NET 6 SDK 以降で使用できます。

レコードの特性

レコードを定義するには、record キーワードを使用して型を宣言するか、class または struct 宣言を変更します。 必要に応じて、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 であり、不変である必要があります。 HighTemp および LowTemp プロパティは "init 専用プロパティ" であり、これは、コンストラクター内で、またはプロパティ初期化子を使用して、設定できることを意味します。 位置指定パラメーターを読み取り/書き込みにする場合は、readonly record struct ではなく record struct を宣言します。 DailyTemperature 型には、2 つのプロパティと一致する 2 つのパラメーターを持つ "プライマリ コンストラクター" もあります。 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 の場合、その日は 5 冷房度日数で、0 暖房度日数です。 逆に、平均気温が 55F の場合は、その日は 10 暖房度日数で、0 冷房度日数です。

これらの数式を、レコード型の小さな階層として表すことができます。つまり、度日を表す 1 つの抽象型と、暖房度日数と冷房度日数のための 2 つの具象型です。 これらの型は、位置指定レコードにすることもできます。 それらは、プライマリ コンストラクターの引数として、基準温度と一連の毎日の気温レコードを受け取ります。

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 レコードは、HeatingDegreeDays レコードと CoolingDegreeDays レコードの両方に対する共有基底クラスです。 派生レコードでのプライマリ コンストラクターの宣言により、基本レコードの初期化を管理する方法が示されています。 派生レコードにより、基本レコードのプライマリ コンストラクターに含まれるすべてのパラメーターに対するパラメーターが宣言されています。 基本レコードにより、それらのプロパティが宣言されて初期化されます。 派生レコードによってそれらは隠ぺいされませんが、基本レコードで宣言されていないパラメーターのプロパティのみが作成されて初期化されます。 この例では、派生レコードによって新しいプライマリ コンストラクター パラメーターが追加されることはありません。 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 }

コンパイラ合成メソッドを定義する

コードで、その期間における暖房度日数と冷房度日数の正しい値を計算します。 ただし、この例では、レコードに対する合成メソッドの一部を置き換える必要がある理由を示します。 クローン メソッドを除き、あるレコード型でのどのコンパイラ合成メソッドについても、独自のバージョンを宣言できます。 クローン メソッドの名前はコンパイラによって生成され、別の実装を提供することはできません。 これらの合成メソッドには、コピー コンストラクター、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 により、レコード型の各プロパティに関する情報が文字列に追加されます。 コントラクトでは、表示にメンバーを追加するための基本レコードが必要であり、派生メンバーによってそれらのメンバーが追加されるものと想定されています。 各レコード型により、HeatingDegreeDays に対する次の例のような ToString のオーバーライドが合成されます。

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 を宣言することを思い出してください。 前に示した HeatingDegreeDaysCoolingDegreeDays の宣言をもう一度見てみましょう。 追加されたメンバーにより、レコードの値に対する計算は行われますが、状態は変更されません。 位置指定レコードを使用すると、変更不可能な参照型をより簡単に作成できます。

変更不可能な参照型を作成するということは、非破壊的な変化を使用することを意味します。 withを使用して、既存のレコード インスタンスに似た新しいレコード インスタンスを作成します。 これらの式は、コピーの構築にコピーを変更する割り当てを追加したものです。 結果として、既存のレコードから各プロパティがコピーされ、必要に応じて変更が加えられた、新しいレコード インスタンスが作成されます。 元のレコードは変更されません。

with 式を示すいくつかの機能をプログラムに追加してみましょう。 最初に、同じデータを使用して、成長度日数を計算する新しいレコードを作成してみます。 "成長度日数" の場合は、通常、基準として 41F を使用し、基準を上回る温度を測定します。 同じデータを使用するため、coolingDegreeDays に似た新しいレコードを作成しますが、異なる基準温度を使用します。

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

計算された度数と、より高い基準温度で生成された値を比較できます。 レコードは "参照型" であり、これらのコピーは簡易コピーであることに注意してください。 データの配列はコピーされず、両方のレコードで同じデータが参照されています。 その事実は、他のもう 1 つのシナリオでの利点になります。 成長度日数の場合は、過去 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 型は、レコードの機能を効率的にコピーできる程度の小さな型に使用したい場合に作成します。

レコードの詳細な説明については、レコード型に関する C# 言語のリファレンス記事提案されるレコード型の仕様レコード構造体の仕様を参照してください。