レコード は、 値ベースの等価性を使用する型です。 レコードは参照型または値型として定義できます。 レコード・タイプ定義が同一の場合、レコード・タイプの 2 つの変数は等しく、すべてのフィールドに対して両方のレコードの値が等しい場合は等しくなります。 参照されるオブジェクトが同じクラス型であり、変数が同じオブジェクトを参照している場合、クラス型の 2 つの変数は等しくなります。 値ベースの等価性は、レコード型で必要なその他の機能を意味します。 コンパイラは、recordではなくclassを宣言すると、それらのメンバーの多くを生成します。 コンパイラは、 record struct 型に対して同じメソッドを生成します。
このチュートリアルでは、以下の内容を学習します。
-
record型にclass修飾子を追加するかどうかを決定します。 - レコード型と位置指定レコード型を宣言します。
- レコード内でコンパイラによって生成されたメソッドを、あなたのメソッドで置き換えます。
前提条件
- 最新の .NET SDK
- Visual Studio Code エディター
- C# DevKit
レコードの特性
レコードを定義する場合は、record キーワードを使用して型を宣言し、classまたはstruct宣言を変更します。 必要に応じて、 class キーワードを省略して record classを作成できます。 レコードは、値ベースの等値セマンティクスに従います。 値セマンティクスを適用するために、コンパイラはレコード型 ( record class 型と record struct 型の両方) に対して複数のメソッドを生成します。
- Object.Equals(Object) のオーバーライド。
- パラメーターがレコード型である仮想
Equalsメソッド。 - Object.GetHashCode() がオーバーライドされました。
-
operator ==とoperator !=のメソッド。 - レコード型は System.IEquatable<T>を実装します。
レコードは、 Object.ToString()のオーバーライドも提供します。 コンパイラは、 Object.ToString()を使用してレコードを表示するためのメソッドを合成します。 これらのメンバーは、このチュートリアルのコードを記述するときに調べることができます。 レコードは with 式をサポートし、レコードの非破壊的変更を有効にします。
位置 指定レコード は、より簡潔な構文を使用して宣言することもできます。 位置指定レコードを宣言すると、コンパイラによってさらに多くのメソッドが合成されます。
- パラメーターがレコード宣言の位置指定パラメーターと一致するプライマリ コンストラクター。
- プライマリ コンストラクターの各パラメーターのパブリック プロパティ。 これらのプロパティは、 型および
record class型に対してのみ初期化時に設定可能です。record struct型の場合、読み取り/書き込みです。 - レコードからプロパティを抽出する
Deconstructメソッド。
温度データを構築する
データと統計は、レコードを使用するシナリオの 1 つです。 このチュートリアルでは、さまざまな用途で 日数 を計算するアプリケーションを構築します。 度日数は、日、週、または月の期間にわたる熱エネルギー(またはその不足)の指標です。 度日数を使用して、エネルギー使用量を追跡および予測します。 より暑い日は、より多くの空調を意味し、より寒い日は、より多くの炉の使用を意味します。 学位日は、植物の個体数を管理し、季節の変化に応じて植物の成長に関連付けるのに役立ちます。 学位日は、気候に合わせて移動する種の動物の移動を追跡するのに役立ちます。
式は、特定の日の平均温度とベースライン温度に基づいています。 経時的な度日の計算を行うには、一定期間の毎日の高温と低温が必要です。 まず、新しいアプリケーションを作成します。 新しいコンソール アプリケーションを作成します。 "DailyTemperature.cs" という名前の新しいファイルに新しいレコードの種類を作成します。
public readonly record struct DailyTemperature(double HighTemp, double LowTemp);
上記のコードは 位置指定レコードを定義します。
DailyTemperature レコードは、これを継承するつもりがないため readonly record struct であり、不変である必要があります。
HighTempプロパティとLowTemp プロパティは init のみのプロパティです。つまり、コンストラクターまたはプロパティ初期化子を使用して設定できます。 位置指定パラメーターを読み取り/書き込みにする場合は、record structではなくreadonly record structを宣言します。
DailyTemperature型には、2 つのプロパティに一致する 2 つのパラメーターを持つプライマリ コンストラクターもあります。 プライマリ コンストラクターを使用して、 DailyTemperature レコードを初期化します。 次のコードは、複数の DailyTemperature レコードを作成して初期化します。 1 つ目では、名前付きパラメーターを使用して、 HighTemp と LowTempを明確にします。 残りの初期化子は、位置指定パラメーターを使用して HighTemp と LowTempを初期化します。
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 F を使用します。 加熱や冷却が不要な温度です。 1 日の平均温度が 70 F の場合、その日は 5 つの冷却度日とゼロ加熱度日です。 逆に、平均温度が55Fの場合、その日は10加熱度日と0冷却度日である。
これらの式は、抽象的な度日型と、暖房度日および冷却度日の 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 }
コンパイラ合成メソッドを定義する
コードで、その期間における暖房度日数と冷房度日数の正しい値を計算します。 ただし、この例では、レコードの合成メソッドの一部を置き換える必要がある理由を示します。 複製メソッドを除き、レコード型でコンパイラによって合成されたメソッドの独自のバージョンを宣言できます。 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();
}
コレクションの型を出力しないPrintMembers レコードで、DegreeDays メソッドを宣言します。
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
return true;
}
シグネチャは、コンパイラのバージョンに一致する virtual protected メソッドを宣言します。 アクセサーメソッドが間違っている場合は心配しないでください。プログラミング言語が正しいシグネチャを強制します。 合成されたメソッドの正しい修飾子を忘れた場合、コンパイラは正しいシグネチャを取得するのに役立つ警告またはエラーを発行します。
ToString メソッドは、レコード型でsealedとして宣言できます。 これにより、派生レコードが新しい実装を提供できなくなります。 派生レコードには、引き続き PrintMembers オーバーライドが含まれます。 レコードのランタイム型を表示したくない場合は、 ToString をシールします。 前の例では、記録が暖房度日または冷房度日を測定していた場所の情報を失うことになります。
非破壊な変化
位置指定レコード クラスの合成されたメンバーは、レコードの状態を変更しません。 目標は、変更できないレコードをより簡単に作成できることです。 変更できないレコード構造体を作成する readonly record struct を宣言することを忘れないでください。
HeatingDegreeDaysとCoolingDegreeDaysについては、前述の宣言をもう一度見てください。 追加されたメンバーは、レコードの値に対して計算を実行しますが、状態は変更しません。 位置指定レコードを使用すると、変更できない参照型を簡単に作成できます。
変更できない参照型を作成することは、非破壊的な変更を使用することを意味します。
with式を使用して、既存のレコード インスタンスに似た新しいレコード インスタンスを作成します。 これらの式は、コピーを変更する追加の割り当てを持つコピー構造です。 結果は、各プロパティが既存のレコードからコピーされ、必要に応じて変更された新しいレコード インスタンスになります。 元のレコードは変更されません。
with式を示すいくつかの機能をプログラムに追加しましょう。 最初に、同じデータを使用して、成長度日数を計算する新しいレコードを作成してみます。
成長度日数の場合は、通常、基準として 41 F を使用し、基準を上回る温度を測定します。 同じデータを使用するには、 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# 言語リファレンス記事を参照してください。
.NET