C# 9.0で導入されたrecord型は、データのまとまりを表現することに特化した参照型です。
従来のclassを使用する場合、データの等価性判定(値がすべて同じなら「等しい」とみなすこと)や、不変性(Immutability)を担保するためには、多くのボイラープレートコードを記述する必要がありました。record型を使用することで、簡潔な構文で「値としてのセマンティクス」を持つオブジェクトを定義できます。
ここでは、商品管理システムを題材に、record型の基本的な定義方法、継承、そしてクラス型とは異なる等価判定の挙動について解説します。
record型の特徴とメリット
record型を採用する主なメリットは以下の通りです。
- 値ベースの等価性: インスタンスの参照(メモリアドレス)ではなく、プロパティの全値が一致しているかで等価性を判定します。
- 不変性(Immutability): プライマリコンストラクタを使用する場合、プロパティはデフォルトで初期化のみ可能な(init-only)状態となります。
- 簡潔な記述: コンストラクタ、分解(Deconstruct)、
ToString、Equals、GetHashCodeなどが自動生成されます。
実践的なコード例:商品データの定義と等価比較
以下のコードは、基本となる「商品」レコードと、それを継承した「書籍」レコードを定義し、それぞれの比較結果を確認する例です。
using System;
namespace ProductManagement
{
// 基本となる商品レコード
// プライマリコンストラクタを使ってプロパティを宣言します。
// これにより、Id と Name は自動的に init-only プロパティとなります。
public record Product(int Id, string Name);
// Productを継承した書籍レコード
public record Book(int Id, string Name, string Author) : Product(Id, Name);
class Program
{
static void Main()
{
// 1. 同じ値を持つ別のインスタンスを作成
var product1 = new Product(1001, "高性能マウス");
var product2 = new Product(1001, "高性能マウス");
// 2. 継承したレコードのインスタンスを作成
var book1 = new Book(2001, "C#入門", "山田太郎");
// 参照のコピー(同じインスタンスを指す)
var productReference = product1;
// --- 検証結果の出力 ---
// 自動生成されたToString()の確認
// クラス型と異なり、プロパティの値が出力されます。
Console.WriteLine("--- ToString() の結果 ---");
Console.WriteLine($"product1: {product1}");
Console.WriteLine($"book1 : {book1}");
Console.WriteLine();
Console.WriteLine("--- 等価性の判定 ---");
// A. 値ベースの等価判定 (== 演算子)
// 別のインスタンスであっても、プロパティの値がすべて同じであれば True となります。
bool isValueEqual = (product1 == product2);
Console.WriteLine($"product1 == product2 : {isValueEqual}");
// B. 参照ベースの等価判定 (Object.ReferenceEquals)
// 値が同じでも、メモリ上のインスタンスとしては別物なので False となります。
bool isReferenceEqual = Object.ReferenceEquals(product1, product2);
Console.WriteLine($"ReferenceEquals(p1, p2) : {isReferenceEqual}");
// C. 参照が同じ場合の判定
Console.WriteLine($"product1 == productReference : {product1 == productReference}");
Console.WriteLine($"ReferenceEquals(p1, pRef) : {Object.ReferenceEquals(product1, productReference)}");
// D. 型が異なる場合の判定
// プロパティの一部が一致していても、型が異なれば False となります。
// (注: コンパイル警告が出る場合がありますが、比較自体は可能です)
Console.WriteLine($"product1 != book1 : {product1 != book1}"); // Trueになる(等しくない)
Console.WriteLine();
Console.WriteLine("--- ハッシュコードの確認 ---");
// 値が同じであれば、GetHashCodeの結果も同じになります。
Console.WriteLine($"product1 Hash: {product1.GetHashCode()}");
Console.WriteLine($"product2 Hash: {product2.GetHashCode()}");
}
}
}
実行結果
--- ToString() の結果 ---
product1: Product { Id = 1001, Name = 高性能マウス }
book1 : Book { Id = 2001, Name = C#入門, Author = 山田太郎 }
--- 等価性の判定 ---
product1 == product2 : True
ReferenceEquals(p1, p2) : False
product1 == productReference : True
ReferenceEquals(p1, pRef) : True
product1 != book1 : True
--- ハッシュコードの確認 ---
product1 Hash: 1860543227
product2 Hash: 1860543227
技術的なポイント
1. 参照型でありながら値として振る舞う
recordは内部的にはclassとしてコンパイルされます(record structと明示しない限り)。しかし、コンパイラが自動的にIEquatable<T>の実装や==演算子のオーバーロードを追加するため、開発者は「値オブジェクト(Value Object)」として自然に扱うことができます。
2. インスタンスの識別
通常のクラスでは、IDなどのプロパティが同じでもインスタンスが異なれば「別物」と扱われます。しかし、上記の実行結果にある通り、record型ではproduct1 == product2がTrueになります。これにより、DTO(Data Transfer Object)やAPIレスポンスの比較などが非常に容易になります。
3. ハッシュコードの自動生成
GetHashCodeメソッドもプロパティの値に基づいて計算されるようオーバーライドされます。そのため、HashSet<T>やDictionary<TKey, TValue>のキーとしてrecord型を使用した場合、値が同じであれば同じキーとして正しく認識されます。
まとめ
record型を使用することで、データの入れ物としてのクラス定義を簡略化できるだけでなく、値の等価性に基づいた堅牢な設計が可能になります。ドメインモデルの値オブジェクトや、不変性が求められるデータ構造において、積極的に採用すべき機能です。
