record
修飾子を使って、データをカプセル化するための組み込み機能を提供する参照型を定義します。 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 class
では "readonly record struct
" です。 では "record struct
" です。
この記事の残りの部分で、record class
と record struct
の両方の型について説明します。 相違点について、各セクションで詳しく説明します。 record class
か record struct
かを決定するのと同じように、class
と struct
のどらかを決定する必要があります。 "レコード" という用語は、すべてのレコードの種類に適用される動作を記述するために使用されます。 record struct
または record class
は、それぞれ構造体型またはクラス型にのみ適用される動作を説明するために使用されます。
プロパティとフィールド定義の位置指定構文
位置指定パラメーターを使用して、レコードのプロパティを宣言したり、プロパティまたはフィールド値を初期化したりできます。 次の例では、2 つの位置プロパティを持つレコードを作成します。
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
型の場合: 読み取り/書き込みプロパティ。
- パラメーターがレコード宣言の位置指定パラメーターと一致するプライマリ コンストラクター。
- レコード構造体型の場合、各フィールドをその既定値に設定するパラメーターなしのコンストラクター。
- レコード宣言で指定された各定位置指定パラメーターの
Deconstruct
パラメーターを使用するout
メソッド。 このメソッドにより、位置指定構文を使用して定義されたプロパティは分解されます。標準のプロパティ構文を使用して定義されたプロパティは無視されます。
コンパイラでレコード定義から作成されるこれらの要素のいずれかに、属性の追加が必要になることがあります。 位置指定レコードのプロパティに適用する任意の属性に "ターゲット" を追加できます。 次の例では、System.Text.Json.Serialization.JsonPropertyNameAttribute レコードの各プロパティに Person
を適用します。 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
アクセサーのいずれかの実装を提供することもできます。 ソースでメンバーを宣言する場合は、レコードの位置指定パラメーターからメンバーを初期化する必要があります。 プロパティが自動的に実装されるプロパティの場合は、プロパティを初期化する必要があります。 ソースにバッキング フィールドを追加する場合は、バッキング フィールドを初期化する必要があります。 生成されたデコンストラクターは、プロパティまたはフィールドの定義を使用します。 たとえば、次の例では、位置指定レコード FirstName
の LastName
と public
のプロパティを宣言していますが、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, string Id)
{
internal readonly string Id = Id; // this.Id set to parameter 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; } = [];
};
位置パラメーターからコンパイラによって生成されるプロパティは public
。 明示的に宣言するプロパティに対してアクセス修飾子を宣言します。
不変性
位置指定レコードクラス と、位置指定読み取り専用レコード構造体 は、init-only プロパティを宣言します。 "位置指定レコード構造体" を使用して、読み取り/書き込みプロパティを宣言します。 前のセクションで示したように、これらの既定値のいずれかをオーバーライドできます。
不変性は、データ中心の型をスレッドセーフにする必要がある場合またはハッシュ テーブル内で変化のないハッシュ コードに依存している場合に役立ちます。 ただし、不変性は、すべてのデータ シナリオに適しているわけではありません。 たとえば、Entity Framework Core では、不変のエンティティ型を使用した更新がサポートされていません。
位置指定パラメーター (record class
と readonly 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
}
レコード型に固有の機能は、コンパイラによって合成されたメソッドによって実装されます。このようなメソッドのいずれを使用してオブジェクトの状態を変更しても、不変性は損なわれません。 指定しない限り、合成されたメソッドは、record
、record struct
、および readonly record struct
宣言に対して生成されます。
値の等価性
オーバーライドしない、または等値メソッドを置き換えない場合は、宣言する型で等価性の定義方法を制御します。
class
型の場合、メモリ内の同じオブジェクトを参照していれば、2 つのオブジェクトは等しい。struct
型の場合、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。record
修飾子を持つ型の場合 (record class
、record struct
、readonly 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) 静的メソッドの基礎として使用されます。
virtual
、sealed
、または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
式により、位置指定プロパティ、または標準のプロパティ構文を使用して作成されたプロパティを設定できます。 明示的に宣言されたプロパティの場合、init
式で変更される set
または with
アクセサーが必要です。
with
式の結果は "with
" です。つまり、参照プロパティの場合、インスタンスへの参照のみがコピーされます。 元のレコードとコピーの両方が、同じインスタンスへの参照になります。
record class
にこの機能を実装するために、コンパイラによって、クローン メソッドとコピー コンストラクターが合成されます。 仮想クローン メソッドからは、コピー コンストラクターによって初期化された新しいレコードが返されます。 with
式を使用すると、クローン メソッドを呼び出すコードがコンパイラによって作成され、with
式で指定されたプロパティが設定されます。
別のコピー動作が必要な場合は、record class
に独自のコピー コンストラクターを作成できます。 こうすると、コンパイラによって合成されません。 レコードが private
の場合はコンストラクターが sealed
になり、それ以外の場合は protected
になります。 record struct
型のコピー コンストラクターは、コンパイラによって合成されません。 記述することはできますが、with
式のための呼び出しは、コンパイラによって生成されません。 record struct
の値は、代入時にコピーされます。
クローン メソッドをオーバーライドすることはできず、どのレコード型にも Clone
という名前のメンバーを作成することはできません。 クローン メソッドの実際の名前はコンパイラによって生成されます。
表示用の組み込みの書式設定
レコード型には、パブリック プロパティとフィールドの名前と値を表示する、コンパイラによって生成された ToString メソッドがあります。 ToString
メソッドからは、次の形式の文字列が返されます。
<record type name> { <property name> = <value>, <property name> = <value>, ...}
<value>
の出力される文字列は、プロパティの型に対して ToString() によって返される文字列です。 次の例で ChildNames
は System.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
の書式設定」です。 ToString
の実装には、sealed
修飾子を含めることができます。これにより、コンパイラは派生レコードに対して ToString
実装を合成できなくなります。 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
}
この例では、インスタンスが Person
または Student
のいずれかの派生型である場合でも、すべての変数が Teacher
として宣言されています。 インスタンスのプロパティが同じであり、プロパティ値も同じです。 ただし、両方とも student == teacher
型の変数であっても、False
から Person
が返されます。また、いずれかが student == student2
変数でもう一方が True
変数であっても Person
から Student
が返されます。 等価性テストは、変数の宣言された型ではなく、実際のオブジェクトのランタイム型に依存します。
この動作を実装するために、コンパイラにより、レコード型と一致する EqualityContract
オブジェクトを返す Type プロパティが合成されます。 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
メソッドの独自の実装を用意できます。 その場合は、次のシグネチャを使用します。
sealed
から派生した (基本レコードを宣言しない)object
レコードの場合: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 }
}
注意
基底レコードが 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 class
は class
制約を満たします。 record struct
は struct
制約を満たします。 詳細については、「型パラメーターの制約」を参照してください。
C# 言語仕様
詳細については、C# 言語仕様の「クラス」セクションを参照してください。
これらの機能の詳細については、機能の提案に関する次の記述を参照してください。
関連項目
.NET