C#の開発において、データの保持を目的としたオブジェクトの設計は非常に頻繁に行われます。

かつては、等価性の判定や不変性を実現するために、開発者は膨大なボイラープレートコード(定型的な記述)を記述する必要がありました。

しかし、C# 9.0で導入され、その後のバージョンで強化されたrecord型は、この状況を劇的に改善しました。

本記事では、record型の基礎から、クラスや構造体との決定的な違い、そして不変オブジェクトを設計する際のベストプラクティスまで、プロフェッショナルな視点で詳しく解説します。

record型とは何か:データ中心の設計を可能にする新機能

C#におけるrecord型は、一言で言えば「データそのものを表現するための特別な型」です。

従来のclass(クラス)は、オブジェクトが「何をするか」という振る舞いや、カプセル化された状態の管理に主眼が置かれていました。

これに対し、record型はオブジェクトが「どのようなデータを持っているか」という値そのものに焦点を当てています。

record型の最大の特徴は、コンパイラが自動的に値に基づく等価性(Value-based equality)を判定するためのメソッドを生成してくれる点にあります。

これにより、同じプロパティ値を持つ2つの異なるインスタンスを「同じもの」として扱うことが容易になります。

まずは、最もシンプルなrecordの定義方法を見てみましょう。

C#
using System;

// positional record(位置指定レコード)による簡潔な定義
public record Employee(int Id, string Name, string Department);

public class Program
{
    public static void Main()
    {
        // インスタンスの生成
        var emp1 = new Employee(1, "田中 太郎", "開発部");
        var emp2 = new Employee(1, "田中 太郎", "開発部");

        // 内容の表示(ToStringがオーバーライドされている)
        Console.WriteLine($"emp1: {emp1}");
        
        // 値に基づく等価性の確認
        Console.WriteLine($"emp1 == emp2: {emp1 == emp2}"); // True
        Console.WriteLine($"ReferenceEquals: {ReferenceEquals(emp1, emp2)}"); // False
    }
}
実行結果
emp1: Employee { Id = 1, Name = 田中 太郎, Department = 開発部 }
emp1 == emp2: True
ReferenceEquals: False

このコードから分かるように、newによって生成された別々のメモリ領域を指すインスタンスであっても、プロパティの値が同一であれば等価と判定されます。

これがrecord型の核心的なメリットです。

クラス、構造体、record型の決定的な違い

C#には、データを保持するための選択肢として「クラス」「構造体(struct)」「record」の3つが存在します。

これらを適切に使い分けるためには、それぞれの特性を深く理解する必要があります。

1. 参照型か値型か

デフォルトのrecord(または record class)は参照型です。

これはヒープ領域にメモリが確保されることを意味します。

一方で、C# 10.0から導入された record struct値型であり、スタック領域(または親オブジェクトの一部)に配置されます。

2. 等価性の判定基準

ここが最も重要な違いです。

  • class: デフォルトでは「参照の等価性」に基づきます。つまり、メモリ上の同じ場所を指していなければ、プロパティが同じでも false となります。
  • struct: 「値の等価性」に基づきますが、リフレクションを使用するためパフォーマンス上の懸念がある場合があり、手動での Equals オーバーライドが推奨されます。
  • record: コンパイラが効率的な「値の等価性」判定ロジックを自動生成します。

3. 可変性(Mutability)

  • class: デフォルトで可変(Mutable)です。
  • record: 位置指定構文(後述)を使用した場合、デフォルトで不変(Immutable)になります。プロパティの値は初期化時のみ設定可能で、後から変更することはできません。

比較をまとめると、以下の表のようになります。

特徴classrecord (class)record structstruct
型の分類参照型参照型値型値型
等価性の判定参照ベース値ベース値ベース値ベース
デフォルトの可変性可変不変(推奨)可変(標準は可変)可変
継承可能record間のみ可能不可不可
ToStringの出力型名のみプロパティ値を含むプロパティ値を含む型名のみ

不変オブジェクトの設計:init専用プロパティとwith式

現代的なソフトウェア開発、特にマルチスレッド環境や関数型プログラミングの影響を受けた設計では、オブジェクトの不変性(Immutability)が重視されます。

record型はこの不変オブジェクトの設計を強力にサポートします。

init専用プロパティ

不変オブジェクトを実現するために、C# 9.0で導入されたのが init アクセサです。

これは「オブジェクトの初期化時のみ値を設定できる」という制限を加えるものです。

C#
public record Product
{
    public string Name { get; init; } // 初期化時のみ変更可能
    public decimal Price { get; init; }
}

// 使用例
var p = new Product { Name = "Laptop", Price = 150000 };
// p.Price = 160000; // コンパイルエラー:init専用プロパティは変更不可

非破壊的変更:with式

不変オブジェクトは値を変更できないため、一部のデータだけを変えた新しいインスタンスが必要な場合があります。

これを効率的に行うのが with式 です。

C#
var original = new Employee(1, "鈴木", "営業部");

// 名前だけ変更した新しいインスタンスを作成(元のoriginalは変更されない)
var updated = original with { Name = "佐藤" };

Console.WriteLine($"Original: {original}");
Console.WriteLine($"Updated : {updated}");
実行結果
Original: Employee { Id = 1, Name = 鈴木, Department = 営業部 }
Updated : Employee { Id = 1, Name = 佐藤, Department = 営業部 }

注意点として、with 式は「浅いコピー(Shallow Copy)」を行います。

プロパティが参照型(例えば List<T> など)を含んでいる場合、そのリストの中身までは複製されず、同じリストを参照することになります。

位置指定レコード(Positional Records)の利便性

record型を定義する際、最も簡潔でよく使われるのが位置指定構文です。

これにより、わずか1行で「コンストラクタ」「プロパティ」「非構築化(Deconstruction)」を定義できます。

C#
public record User(int Id, string UserName, string Email);

この1行の記述により、コンパイラは内部的に以下のものを生成します。

  1. Id, UserName, Email という名称の init 専用プロパティ。
  2. それらの値を初期化するコンストラクタ。
  3. インスタンスから各値を取り出すための Deconstruct メソッド。
  4. プロパティ値を整形して表示する ToString のオーバーライド。
  5. 値に基づく等価性判定メソッド(Equals, GetHashCode, ==, !=)。

非構築化(Deconstruction)の利用

位置指定レコードは、変数を一度に分解して受け取ることができます。

C#
var user = new User(101, "Alice", "alice@example.com");

// 非構築化による値の取り出し
var (id, name, mail) = user;

Console.WriteLine($"ID: {id}, Name: {name}");

この機能により、データ処理のコードが非常に読みやすくなります。

record struct:パフォーマンスを重視する場合の選択肢

C# 10.0以降、record のメリット(簡潔な記述、値ベースの等価性)を値型でも享受できるようになりました。

これが record struct です。

通常の record(record class)は参照型であるため、ガベージコレクション(GC)の対象となります。

大量の小さなデータを頻繁に生成・破棄するようなゲーム開発や高頻度取引システムなどでは、ヒープへの割り当てを避けるために record struct が適しています。

C#
// デフォルトで可変(Mutable)なrecord struct
public record struct Point(double X, double Y);

// 不変(Immutable)なrecord structを定義する場合
public readonly record struct ReadOnlyPoint(double X, double Y);

重要な違い: record class の位置指定構文はデフォルトで init(不変)になりますが、record struct の位置指定構文はデフォルトで通常の set(可変)になります。

意図的に不変にしたい場合は、型定義に readonly を付ける必要があります。

record型の継承と等価性の詳細

record型はクラスと同様に継承をサポートしていますが、そこには record 特有の仕組みがあります。

特に、異なる型同士の比較において record は安全に設計されています。

C#
public record Person(string Name);
public record Teacher(string Name, string Subject) : Person(Name);

var p = new Person("山田");
var t = new Teacher("山田", "数学");

Console.WriteLine(p == t); // False(型が異なるため)

通常のクラスで等価性を自作すると、継承関係がある場合に「基本クラスとして比較すると一致してしまう」といったバグが発生しやすいですが、record型ではコンパイラが EqualityContract というプロパティを生成し、実行時の型が一致しているかどうかもチェックに含めるため、安全に比較が行われます。

実践的な活用シーン

record型をどのような場面で使うべきか、具体的なユースケースを挙げます。

1. DTO(Data Transfer Object)

APIのレスポンスやデータベースからの取得結果を格納するオブジェクトです。

これらは「データの入れ物」としての役割が強く、record型との相性が抜群です。

2. DDD(ドメイン駆動設計)における「値オブジェクト」

「金額(Currency + Amount)」や「住所」のように、値そのものが意味を持つオブジェクトを不変として定義する際に最適です。

3. メッセージングとイベント駆動

イベントソーシングやアクターモデルにおいて、システム間を流れる「イベントメッセージ」は、後から書き換えられてはいけないため、不変な record 型が推奨されます。

4. 設定情報の保持

アプリケーションの起動時に読み込む設定ファイルの内容を保持するクラスを record で定義することで、実行中に誤って設定値が書き換わるリスクを排除できます。

パフォーマンスと注意点

record型は非常に便利ですが、盲目的にすべてのクラスを置換すべきではありません。

  • コンパイル後のコード量: record型を使用すると、コンパイラが裏側で多くのメソッドを自動生成します。実行バイナリのサイズに極端に厳しい制約がある場合は注意が必要です(通常は無視できるレベルです)。
  • コレクションの比較: 前述の通り、プロパティに List<T> などの参照型が含まれる場合、等価性判定はその「リストのインスタンスが同じか」のみを見ます。リストの「中身」が同じかどうかまでは判定しません。もし中身まで比較したい場合は、Equals メソッドを手動でオーバーライドする必要があります。

まとめ

C#のrecord型は、単なる「コードを短く書くための機能」に留まらず、堅牢で予測可能なプログラムを設計するための重要なツールです。

  • 値に基づく等価性により、データの比較が直感的かつ正確になります。
  • 不変性(Immutability)をデフォルトとして扱うことで、副作用の少ないコードを記述できます。
  • with式位置指定構文により、不変オブジェクトの操作が極めて簡潔になります。
  • record structの導入により、パフォーマンスが要求される場面でも record の利点を活用できます。

従来の class は、複雑なロジックや状態遷移を持つ「振る舞いの中心」となるオブジェクトに使い、record はシステム内を流れる「データそのもの」を表現するために使う。

この明確な使い分けを意識することで、あなたのC#コードはより洗練され、メンテナンス性の高いものへと進化するでしょう。

モダンなC#開発において、record型の習得はもはや必須と言っても過言ではありません。