C# レコードの種類

ヒント

ソフトウェアの開発は初めてですか? 最初に、 作業の開始 に関するチュートリアルから始めます。 必要がある場合、組み込みの等価性を持つ簡潔なデータ型としてレコードに出会うことがあるでしょう。

別の言語で経験がありますか? C# レコードは、Kotlin のデータ クラスや Scala のケース クラスに似ています。 これらは、コンパイラによって生成された等価性、 ToString、およびコピー セマンティクスを使用して、データを格納するために最適化された型です。 C# 固有のパターンの record classrecord struct および with のセクションをスキミングします。

record キーワードは、classまたはstructに適用する修飾子です。 ToString式を使用して、値の等価性、書式設定されたwith、非破壊的な変更を生成するようにコンパイラに指示します。 基になる型(クラスまたは構造体)は、インスタンスが参照セマンティクスと値セマンティクスのどちらを使用するかを決定します。 record修飾子は、これらのセマンティクスの上にデータに優しい動作を追加します。 型のプライマリ ロールがデータを格納していて、同じ値を持つ 2 つのインスタンスが等しいと見なす必要がある場合は、レコードを使用します。

レコードを使用するタイミング

次のすべての条件に該当する場合は、レコードを使用します。

  • 型の主な役割は、データの格納です。
  • 同じ値を持つ 2 つのインスタンスが等しい必要があります。
  • 不変性が必要です (特に record class 型の場合)。
  • 手動で書かずに読み取り可能な ToString が必要です。

record classrecord structのいずれかを選択する場合:

  • 継承が必要な場合や、型が大きいためすべての割り当てでのコピーがコスト高になる場合には、record class を使用してください。
  • コピー セマンティクスとスタック割り当てが有益な小さな自己完結型の値には、 record struct を使用します。

Entity Framework Core では、エンティティを追跡するために参照の等価性に依存しているため、エンティティ型のレコードを使用しないでください。 型オプションのより広範な比較については、「 型の種類を選択する」を参照してください。

レコードを宣言する

クラスまたは構造体に record を適用できます。 最も単純な形式では、コンストラクターとプロパティの両方を 1 行で定義する 位置指定パラメーター を使用します。

public record Person(string FirstName, string LastName);

同じ位置指定構文は、 record struct 型に対して機能します。

public record struct Coordinate(double Latitude, double Longitude);

public readonly record struct Temperature(double Celsius)
{
    public double Fahrenheit => Celsius * 9.0 / 5.0 + 32.0;
}

recordのみを記述することは、参照型であるrecord classの短縮形です。 record structを記述すると、値型が作成されます。 どちらの場合も、コンパイラは位置指定パラメーターからプロパティを生成しますが、既定値は異なります。

  • record class: プロパティは init専用です (構築後は変更できません)。
  • record struct: プロパティは既定で読み取り/書き込みです。 readonly (readonly record struct) を追加して、init専用にします。

さらに制御が必要な場合は、標準のプロパティ構文を使用してレコードを書き込むこともできます。 たとえば、 init専用ではなく、プロパティを読み取り/書き込みにするには、次のようにします。

public record Product
{
    public required string Name { get; init; }
    public decimal Price { get; set; }
}

record class 対。 record struct

record修飾子は基になる型のセマンティクスを保持するため、参照を割り当てたり比較したりすると、record classrecord structの動作が異なります。 record classを割り当てると、参照がコピーされます。 どちらの変数も同じオブジェクトを指します。 record structを割り当てるとデータがコピーされるため、1 つの変数に対する変更は他の変数には影響しません。

// Record class — assignment copies the reference
var p1 = new Person("Grace", "Hopper");
var p2 = p1; // p1 and p2 point to the same object:
Console.WriteLine(ReferenceEquals(p1, p2)); // True

// Record struct — assignment copies the data
var c1 = new Coordinate(47.6062, -122.3321);
var c2 = c1;
c2.Longitude = 0.0; // mutating c2 doesn't affect c1
Console.WriteLine(c1.Longitude); // -122.3321
Console.WriteLine(c2.Longitude); // 0

レコード構造体は、コンパイラによって生成される値の等価性も提供します。

var home = new Coordinate(47.6062, -122.3321);
var copy = home;

Console.WriteLine(home);           // Coordinate { Latitude = 47.6062, Longitude = -122.3321 }
Console.WriteLine(home == copy);   // True — value equality

継承が必要な場合、またはインスタンスのサイズが十分に大きいため、コピーにコストがかかる場合は、 record class を選択します。 値型のコピー セマンティクスが適切な小さな自己完結型データの場合は、 record struct を選択します。 値型セマンティクスの詳細については、「 構造体」を参照してください。

値の等価性

record修飾子は、クラスと構造体の両方にコンパイラによって生成されるプロパティごとの等価性を提供します。 次に示すのは、4 種類すべての種類で等価性がどのように機能するかです。

  • プレーン クラス: 既定で 参照の等価性 を使用します。 ==演算子は、データが一致するかどうかではなく、2 つの変数が同じオブジェクトを指しているかどうかを確認します。
  • プレーン構造体: ValueType.Equalsを通じて値の等価性をサポートしますが、既定の実装ではリフレクションが使用されます。これは低速であり、 ==/!= 演算子は生成されません。
  • record class: コンパイラは、すべてのプロパティ値を比較する Equals メソッドと GetHashCode メソッド、および ==/!= 演算子を生成します。 同じデータを持つ 2 つの異なるオブジェクトが等しい。
  • record struct: コンパイラによって生成された等値は record classと同じですが、リフレクションを使用しない場合は、プレーン構造体の等価性よりも高速になります。

次の例は、レコード クラスの等価性を示しています。

// Person is a record type with three properties: FirstName, LastName, and PhoneNumbers.
var phones = new string[] { "555-1234" };
var person1 = new Person("Grace", "Hopper", phones);
var person2 = new Person("Grace", "Hopper", phones);

Console.WriteLine(person1 == person2);              // True
Console.WriteLine(ReferenceEquals(person1, person2)); // False

person1.PhoneNumbers[0] = "555-9999";
Console.WriteLine(person2.PhoneNumbers[0]); // 555-9999 — same array

2 つの Person インスタンスは異なるオブジェクトですが、すべてのプロパティ値が一致するため等しくなります。 配列プロパティは、内容ではなく参照によって比較されます。 配列自体はコピーされないため、共有配列の変更は両方のレコードを通じて表示されます。

with式による非破壊的変異

多くの場合、レコードは不変であるため、作成後にプロパティを変更することはできません。 with式では、1 つ以上のプロパティが変更されたコピーが作成され、元のレコードは変更されません。 このアプローチは、 record class 型と record struct 型の両方で機能します。

var original = new Person("Grace", "Hopper");
var modified = original with { FirstName = "Margaret" };

Console.WriteLine(original); // Person { FirstName = Grace, LastName = Hopper }
Console.WriteLine(modified); // Person { FirstName = Margaret, LastName = Hopper }
Console.WriteLine(original == modified); // False

var copy = original with { };
Console.WriteLine(original == copy); // True

同じ構文は、 record struct 型でも機能します。

var shifted = home with { Longitude = -122.0 };
Console.WriteLine(shifted);        // Coordinate { Latitude = 47.6062, Longitude = -122 }
Console.WriteLine(home == shifted); // False

with式は、既存のインスタンスをコピーし、指定したプロパティの変更を適用します。

位置指定レコードとデコンストラクション

位置指定レコードは、プロパティ値を個々の変数に抽出するために使用する Deconstruct メソッドを生成します。

var (first, last) = person;
Console.WriteLine($"{first} {last}");
// Grace Hopper

分解は、 record class 型と record struct 型の両方で機能します。 割り当て、 foreach ループ、パターン マッチングで使用できます。

レコードの継承

record classは、別のrecord classから継承できます。 レコードは通常のクラスから継承できません。また、クラスはレコードから継承できません。

public record Student(string FirstName, string LastName, int GradeLevel)
    : Person(FirstName, LastName);

値の等価性チェックには実行時の型が含まれるため、同じPersonStudentを持つFirstNameLastNameは等しいとは見なされません。 レコード構造体は継承をサポートしていません。構造体は他の型から継承できないためです。

こちらも参照ください