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

record 修飾子を使って、データをカプセル化するための組み込み機能を提供する参照型を定義します。 C# 10 では、同意語としての record class 構文で参照型を明らかにできます。また、record struct で同様の機能の値の型を定義できます。

レコードにプライマリ コンストラクターを宣言すると、コンパイラにより、プライマリ コンストラクター パラメーター用のパブリック プロパティが生成されます。 レコードに対するプライマリ コンストラクター パラメーターは、"位置パラメーター" と呼ばれます。 コンパイラにより、プライマリ コンストラクターまたは位置パラメーターをミラー化する "位置プロパティ" が作成されます。 record 修飾子を持たない型のプライマリ コンストラクター パラメーター用プロパティは、コンパイラによって合成されません。

次の 2 つの例では、record (または record class) 参照型が示されています。

public record Person(string FirstName, string LastName);
public record Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
};

次の 2 つの例では、値の型 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 Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
};

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

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 は、それぞれ構造体型またはクラス型にのみ適用される動作を説明するために使用されます。 record struct 型は、C# 10 で導入されました。

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

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

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 }
}

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

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

レコード定義を基にコンパイラで作成されるこれらの要素のいずれかに属性を追加したいとします。 位置指定レコードのプロパティに適用する任意の属性に "ターゲット" を追加できます。 次の例では、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> タグを追加します。

生成された自動実装プロパティ定義が目的の内容ではない場合は、同じ名前の独自のプロパティを定義できます。 たとえば、アクセシビリティや変更可能性を変更したり、get または set アクセサーの実装を提供したりすることができます。 ソース内でプロパティを宣言する場合は、レコードの位置指定パラメーターから初期化する必要があります。 プロパティが自動実装プロパティの場合は、プロパティを初期化する必要があります。 ソースにバッキング フィールドを追加する場合は、バッキング フィールドを初期化する必要があります。 生成されたデコンストラクターは、プロパティ定義を使います。 たとえば、次の例では、位置指定レコード 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; } = [];
};

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

不変性

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

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

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

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宣言に対して生成されます。

値の等価性

オーバーライドしない、または等値メソッドを置き換えない場合は、宣言する型で等価性の定義方法を制御します。

  • class 型の場合、メモリ内の同じオブジェクトを参照していれば、2 つのオブジェクトは等しい。
  • struct 型の場合、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。
  • record 修飾子を持つ型の場合 (record classrecord 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) 静的メソッドの基礎として使用されます。

  • virtualsealed、または Equals(R? other)。この場合、R はレコードの種類です。 このメソッドは、IEquatable<T> を実装します。 このメソッドは、明示的に宣言できます。

  • レコードの種類が基本レコードの種類 Base から派生している場合は、Equals(Base? other)。 オーバーライドが明示的に宣言されている場合は、エラーになります。 Equals(R? other) の独自の実装を指定する場合は、GetHashCode の実装も指定してください。

  • Object.GetHashCode() のオーバーライド。 このメソッドは、明示的に宣言できます。

  • 演算子 == および != のオーバーライド。 演算子が明示的に宣言されている場合は、エラーになります。

  • レコードの種類が基本レコードの種類から派生している場合は、protected override Type EqualityContract { get; };。 このプロパティは、明示的に宣言できます。 詳しくは、「継承階層の等価性」をご覧ください。

レコードの種類に、明示的に宣言できる合成メソッドのシグネチャと一致するメソッドがある場合、コンパイラによってそのメソッドが合成されることはありません。

非破壊な変化

何らかの変更を加え、インスタンスをコピーする必要がある場合は、with 式を使用して "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 式の結果は "with" です。つまり、参照プロパティの場合、インスタンスへの参照のみがコピーされます。 元のレコードとコピーの両方が、同じインスタンスへの参照になります。

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

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

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

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

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

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

<value> の出力される文字列は、プロパティの型に対して ToString() によって返される文字列です。 次の例で ChildNamesSystem.Array であり、ToString により 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 修飾子が含まれる場合があります。 record 型の階層全体で一貫性のある文字列表現を作成できます。 (派生レコードには、派生したすべてのプロパティに対して 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 であるため効率的に sealed されます。

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

派生レコードの 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 レコードの場合 (囲む型が sealed であるためメソッドは実質的に sealed であることにご注意ください): protected 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
}

ジェネリック制約

record キーワードは、class 型または struct 型の修飾子です。 record 修飾子の追加には、この記事で先ほど説明した動作が含まれます。 型がレコードであることを要求するジェネリック制約はありません。 record classclass 制約を満たします。 record structstruct 制約を満たします。 詳細については、「型パラメーターの制約」を参照してください。

C# 言語仕様

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

これらの機能の詳細については、機能の提案に関する次の記述を参照してください。

関連項目