レコード (C# リファレンス)

C#9 以降では、record キーワードを使用して、データをカプセル化するための組み込み機能を提供する参照型を定義します。 位置指定パラメーターまたは標準のプロパティ構文を使用して、不変のプロパティを持つレコード型を作成できます。

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; } = default!;
    public string LastName { get; init; } = default!;
};

また、変更可能なプロパティとフィールドを使用してレコード型を作成することもできます。

public record Person
{
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;
};

レコードは変更可能ですが、これらは、不変のデータ モデルをサポートすることを主な目的としています。 レコード型には次の機能があります。

構造型を使用して、値の等価性があり、動作がほとんどない、またはまったくないデータ中心の型を設計することもできます。 C# 10 以降では、位置指定パラメーターまたは標準プロパティ構文のいずれかを使用して record struct 型を定義できます。

public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
    public double X {  get; init; }
    public double Y {  get; init; }
    public double Z {  get; init; }
}

レコード構造体も、位置指定レコード構造体と、位置指定パラメーターがないレコード構造体の両方で変更可能です。

public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

上記の例は、参照型であるレコードと値型であるレコードの違いを示しています。

  • record または record class を使用して、参照型を宣言します。 class キーワードは閲覧者にとってわかりやすくなりますが、省略可能です。 record struct を使用して、値の型を宣言します。
  • 位置指定プロパティは、record classreadonly record struct では "不変" です。 record struct では "変更可能" です。

この記事の残りの部分で、record classrecord struct の両方の型について説明します。 相違点について、各セクションで詳しく説明します。 classstruct かを決定するのと同じように、record classrecord struct のどらかを決定する必要があります。 "レコード" という用語は、すべてのレコードの種類に適用される動作を記述するために使用されます。 record struct または record class は、それぞれ構造体型またはクラス型にのみ適用される動作を説明するために使用されます。

プロパティ定義の位置指定構文

位置指定パラメーターを使用すると、レコードのプロパティを宣言し、インスタンスを作成するときにプロパティ値を初期化できます。

public record Person(string FirstName, string LastName);

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}

プロパティ定義に位置指定構文を使用すると、コンパイラにより、以下が作成されます。

  • レコード宣言で指定される各位置指定パラメーターのパブリック init 専用自動実装プロパティ。
    • record 型と readonly record struct 型の場合: init 専用プロパティは、コンストラクターで、またはプロパティ初期化子を使用して設定できます。
    • record struct 型の場合: コンストラクター、プロパティ初期化子、または構築後の代入で設定できる読み取り/書き込みプロパティ。
  • パラメーターがレコード宣言の位置指定パラメーターと一致するプライマリ コンストラクター。
  • レコード構造体型の場合、各フィールドをその既定値に設定するパラメーターなしのコンストラクター。
  • レコード宣言で指定された各定位置指定パラメーターの out パラメーターを使用する Deconstruct メソッド。 このメソッドは、2 つ以上の位置指定パラメーターがある場合にのみ指定されます。 このメソッドにより、位置指定構文を使用して定義されたプロパティは分解されます。標準のプロパティ構文を使用して定義されたプロパティは無視されます。

レコード定義を基にコンパイラで作成されるこれらの要素のいずれかに属性を追加したいとします。 位置指定レコードのプロパティに適用する任意の属性に "ターゲット" を追加できます。 次の例では、Person レコードの各プロパティに System.Text.Json.Serialization.JsonPropertyNameAttribute を適用します。 property: ターゲットは、コンパイラによって生成されるプロパティに属性が適用されることを示します。 その他の値として、属性をフィールドに適用する field: と、属性をパラメーターに適用する param: があります。

/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName, 
    [property: JsonPropertyName("lastName")]string LastName);

前の例では、レコードの XML ドキュメント コメントを作成する方法も示してあります。 プライマリ コンストラクターのパラメーターのドキュメントを追加するには、<param> タグを追加します。

生成された自動実装プロパティ定義が目的の内容ではない場合は、同じ名前の独自のプロパティを定義できます。 これを行うと、そのプロパティ定義が生成されたコンストラクターとデコンストラクターに使用されます。 たとえば、次の例では、位置指定レコード publicFirstNameLastName のプロパティを宣言していますが、Id 位置指定パラメーターを internal に制限しています。 この構文は、レコードとレコード構造体の型に使用できます。 宣言されたプロパティの明示的な代入を、対応する位置指定パラメーターに追加する必要があります。

public record Person(string FirstName, string LastName, string Id)
{
    internal string Id { get; init; } = Id;
}

public static void Main()
{
    Person person = new("Nancy", "Davolio", "12345");
    Console.WriteLine(person.FirstName); //output: Nancy

}

レコード型の場合、位置指定プロパティを宣言する必要はありません。 次の例のように、位置指定プロパティを使用せずにレコードを宣言することも、その他のフィールドとプロパティを宣言することもできます。

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; } = Array.Empty<string>();
};

標準のプロパティ構文を使用してプロパティを定義し、アクセス修飾子を省略した場合、プロパティは暗黙的に private になります。

不変性

"位置指定レコード" と "位置指定読み取り専用レコード構造体" を使用して、init 専用プロパティを宣言します。 "位置指定レコード構造体" を使用して、読み取り/書き込みプロパティを宣言します。 前のセクションで示したように、これらの既定値のいずれかをオーバーライドできます。

不変性は、データ中心の型をスレッドセーフにする必要がある場合またはハッシュ テーブル内で変化のないハッシュ コードに依存している場合に役立ちます。 ただし、不変性は、すべてのデータ シナリオに適しているわけではありません。 たとえば、Entity Framework Core では、不変のエンティティ型を使用した更新がサポートされていません。

位置指定パラメーター (record classreadonly record struct) から作成されたか、init アクセサーを指定して作成されたかにかかわらず、init 専用プロパティには、"浅い不変性" があります。 初期化後に、値型プロパティの値または参照型プロパティの参照を変更することはできません。 ただし、参照型プロパティから参照されるデータは変更できます。 次の例は、参照型の不変プロパティ (この場合は配列) の内容が変更可能であることを示しています。

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234

    person.PhoneNumbers[0] = "555-6789";
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}

レコード型に固有の機能は、コンパイラによって合成されたメソッドによって実装されます。このようなメソッドのいずれを使用してオブジェクトの状態を変更しても、不変性は損なわれません。 指定しない限り、合成されたメソッドは、recordrecord struct、および readonly record struct宣言に対して生成されます。

値の等価性

定義する任意の型に対して、Object.Equals(Object) をオーバーライドして、operator == をオーバーロードできます。 Equals をオーバーライドまたは operator == をオーバーロードしない場合は、宣言する型で等価性の定義方法を制御します。

  • class 型の場合、メモリ内の同じオブジェクトを参照していれば、2 つのオブジェクトは等しい。
  • struct 型の場合、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。
  • record 型の場合 (record structreadonly record structを含む)、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。

record struct の等価性の定義は、struct の場合と同じです。 違いは、struct の場合、実装が ValueType.Equals(Object) であり、リフレクションに依存している点です。 レコードの場合、実装はコンパイラによって合成され、宣言されたデータ メンバーを使用します。

一部のデータ モデルでは、参照の等価性が必要です。 たとえば、Entity Framework Core では、概念的に 1 つのエンティティであるものに対して、エンティティ型の 1 つのインスタンスだけが確実に使用されるようにするために、参照の等価性に依存します。 このため、レコードとレコード構造体は、Entity Framework Core でエンティティ型として使用するのに適していません。

次の例は、レコード型の値が等しいことを示しています。

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    var phoneNumbers = new string[2];
    Person person1 = new("Nancy", "Davolio", phoneNumbers);
    Person person2 = new("Nancy", "Davolio", phoneNumbers);
    Console.WriteLine(person1 == person2); // output: True

    person1.PhoneNumbers[0] = "555-1234";
    Console.WriteLine(person1 == person2); // output: True

    Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}

値の等価性を実装するために、コンパイラにより、次のメソッドが合成されます。

  • Object.Equals(Object) のオーバーライド。

    このメソッドは、両方のパラメーターが null でない場合に、Object.Equals(Object, Object) 静的メソッドの基礎として使用されます。

  • パラメーターがレコード型である仮想 Equals メソッド。 このメソッドは、IEquatable<T> を実装します。

  • Object.GetHashCode() のオーバーライド。

  • 演算子 == および != のオーバーライド。

このような合成されたメソッドのいずれかを置き換えるために、独自の実装を作成できます。 レコード型に、いずれかの合成メソッドのシグネチャと一致するメソッドがある場合、コンパイラでそのメソッドは合成されません。

レコード型で Equals の独自の実装を用意する場合は、GetHashCode の実装も用意してください。

非破壊な変化

何らかの変更を加え、インスタンスをコピーする必要がある場合は、with 式を使用して "非破壊的な変化" を実現できます。 with 式を使用すると、指定したプロパティとフィールドが変更された、既存のレコード インスタンスのコピーである新しいレコード インスタンスが作成されます。 次の例に示すように、オブジェクト初期化子構文を使用して、変更する値を指定します。

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
}

public static void Main()
{
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

    Person person2 = person1 with { FirstName = "John" };
    Console.WriteLine(person2);
    // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { PhoneNumbers = new string[1] };
    Console.WriteLine(person2);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { };
    Console.WriteLine(person1 == person2); // output: True
}

with 式により、位置指定プロパティ、または標準のプロパティ構文を使用して作成されたプロパティを設定できます。 位置指定ではないプロパティの場合、with 式で変更される init または set アクセサーが必要です。

with 式の結果は "浅いコピー" です。つまり、参照プロパティの場合、インスタンスへの参照のみがコピーされます。 元のレコードとコピーの両方が、同じインスタンスへの参照になります。

record class にこの機能を実装するために、コンパイラによって、クローン メソッドとコピー コンストラクターが合成されます。 仮想クローン メソッドからは、コピー コンストラクターによって初期化された新しいレコードが返されます。 with 式を使用すると、クローン メソッドを呼び出すコードがコンパイラによって作成され、with 式で指定されたプロパティが設定されます。

別のコピー動作が必要な場合は、record class に独自のコピー コンストラクターを作成できます。 こうすると、コンパイラによって合成されません。 レコードが sealed の場合はコンストラクターが private になり、それ以外の場合は protected になります。 record struct 型のコピー コンストラクターは、コンパイラによって合成されません。 記述することはできますが、with 式のための呼び出しは、コンパイラによって生成されません。 代わりに、コンパイラによって代入が使用されます。

クローン メソッドをオーバーライドすることはできず、どのレコード型にも Clone という名前のメンバーを作成することはできません。 クローン メソッドの実際の名前はコンパイラによって生成されます。

表示用の組み込みの書式設定

レコード型には、パブリック プロパティとフィールドの名前と値を表示する、コンパイラによって生成された ToString メソッドがあります。 ToString メソッドからは、次の形式の文字列が返されます。

<record type name> { <property name> = <value>, <property name> = <value>, ...}

参照型の場合、プロパティ値ではなく、プロパティから参照されるオブジェクトの型名が表示されます。 次の例では、配列は参照型であるため、実際の配列要素値ではなく System.String[] が表示されます。

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

この機能を実装するために、record class 型では、仮想 PrintMembers メソッドと ToString のオーバーライドがコンパイラによって合成されます。 record struct 型では、このメンバーは private です。 ToString のオーバーライドにより、型名の後に左角かっこが続く StringBuilder オブジェクトが作成されます。 プロパティの名前と値を追加する PrintMembers が呼び出され、右角かっこが追加されます。 次の例は、合成されたオーバーライドに含まれているものと似たコードを示しています。

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("Teacher"); // type name
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

PrintMembers または ToString のオーバーライドに独自の実装を用意できます。 たとえば、この記事で後述する「派生レコードに含まれる PrintMembers の書式設定」です。 C# 10 以降でのあなたの ToString の実装には、派生した任意のレコードに対し、コンパイラが ToString 実装が合成されるのを防ぐ sealed 修飾子が含まれる場合があります。 実質的に、これは ToString の出力にランタイム型の情報が含まれないことを意味します (派生レコードでは PrintMembers メソッドをまだ生成するため、すべてのメンバーと値が表示されます)。

継承

このセクションは、record class 型にのみ適用されます。

レコードは、別のレコードから継承できます。 ただし、レコードはクラスから継承できません。また、クラスはレコードから継承できません。

派生レコード型の位置指定パラメーター

派生レコードにより、基本レコードのプライマリ コンストラクターに含まれるすべてのパラメーターに対する位置指定パラメーターが宣言されます。 基本レコードにより、それらのプロパティが宣言されて初期化されます。 派生レコードによってそれらは隠ぺいされませんが、基本レコードで宣言されていないパラメーターのプロパティのみが作成されて初期化されます。

次の例は、位置指定プロパティ構文を使用した継承を示しています。

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

継承階層の等価性

このセクションは、record class 型に適用され、record struct 型には適用されません。 2 つのレコード変数が等しくなるには、ランタイム型が等しくなければなりません。 含まれている変数型は異なっていていても構いません。 継承された等価性の比較を次のコード例に示します。

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(student2 == student); // output: True
}

この例では、インスタンスが Student または Teacher のいずれかの派生型である場合でも、すべての変数が Person として宣言されています。 インスタンスのプロパティが同じであり、プロパティ値も同じです。 ただし、両方とも Person 型の変数であっても、student == teacher から False が返されます。また、いずれかが Person 変数でもう一方が Student 変数であっても student == student2 から True が返されます。 等価性テストは、変数の宣言された型ではなく、実際のオブジェクトのランタイム型に依存します。

この動作を実装するために、コンパイラにより、レコード型と一致する Type オブジェクトを返す EqualityContract プロパティが合成されます。 EqualityContract により、等価性メソッドで、等価性の確認時にオブジェクトのランタイム型を比較できるようになります。 レコードの基本データ型が object の場合、このプロパティは virtual です。 基本データ型が別のレコード型である場合、プロパティはオーバーライドになります。 レコード型が sealed の場合、プロパティは sealed です。

派生型の 2 つのインスタンスを比較する場合、合成された等価性メソッドにより、基本型と派生型のすべてのプロパティの等価性が確認されます。 合成された GetHashCode メソッドには、基本データ型と派生レコード型で宣言されたすべてのプロパティとフィールドからの GetHashCode メソッドが使用されます。

派生レコードの with

with 式の結果は、式のオペランドと同じランタイム型になります。 ランタイム型のすべてのプロパティがコピーされますが、次の例に示すように、コンパイル時型のプロパティのみを設定できます。

public record Point(int X, int Y)
{
    public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
    public int Zderived { get; set; }
};

public static void Main()
{
    Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };

    Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
    Console.WriteLine(p2 is NamedPoint);  // output: True
    Console.WriteLine(p2);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }

    Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
    Console.WriteLine(p3);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}

派生レコードに含まれる PrintMembers の書式設定

派生レコード型の合成された PrintMembers メソッドから、基本実装が呼び出されます。 その結果、次の例に示すように、派生型と基本型の両方のすべてのパブリック プロパティとフィールドが ToString の出力に含まれます。

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

PrintMembers メソッドの独自の実装を用意できます。 その場合は、次のシグネチャを使用します。

  • object から派生した (基本レコードを宣言しない) sealed レコードの場合: private bool PrintMembers(StringBuilder builder);
  • 別のレコードから派生した sealed レコードの場合: protected sealed override bool PrintMembers(StringBuilder builder);
  • sealed ではなく、オブジェクトから派生したレコードの場合: protected virtual bool PrintMembers(StringBuilder builder);
  • sealed ではなく、別のレコードから派生したレコードの場合: protected override bool PrintMembers(StringBuilder builder);

合成された PrintMembers メソッドを置き換えるコードの例を次に示します。1 つはオブジェクトから派生するレコード型用で、もう 1 つは別のレコードから派生するレコード型用です。

public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
    protected virtual bool PrintMembers(StringBuilder stringBuilder)
    {
        stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
        stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
        return true;
    }
}

public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
    : Person(FirstName, LastName, PhoneNumbers)
{
    protected override bool PrintMembers(StringBuilder stringBuilder)
    {
        if (base.PrintMembers(stringBuilder))
        {
            stringBuilder.Append(", ");
        };
        stringBuilder.Append($"Grade = {Grade}");
        return true;
    }
};

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}

注意

C# 10 以降では、基本レコードが ToString メソッドをシールドしている場合に、コンパイラが PrintMembers を合成します。 また、自分独自の PrintMembers の実装を作成することもできます。

派生レコードのデコンストラクターの動作

派生レコードの Deconstruct メソッドからは、コンパイル時型のすべての位置指定プロパティの値が返されます。 変数の型が基本レコードの場合、オブジェクトが派生型にキャストされない限り、基本レコードのプロパティのみが分解されます。 派生レコードに対してデコンストラクターを呼び出す方法の例を次に示します。

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
    Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio

    var (fName, lName, grade) = (Teacher)teacher;
    Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}

ジェネリック制約

型がレコードであることを要求するジェネリック制約はありません。 レコードは class または struct 制約を満たします。 レコード型の特定の階層に制約を設定するには、基底クラスと同じように基本レコードに制約を設定します。 詳細については、「型パラメーターの制約」を参照してください。

C# 言語仕様

詳細については、C# 言語仕様の「クラス」セクションを参照してください。

C# 9 以降で導入された機能の詳細については、次の機能の提案に関するメモを参照してください。

関連項目