記事内に広告が含まれています

C#入門⑨|record(レコード)型の使い方とclassとの違い

C#入門⑨ 基本文法・構文解説

前回は、継承やインターフェースなど「クラスの応用的な使い方」について学びました。

今回は、C# 9.0 で導入された record (レコード)型について扱います。

record型は従来のclass型とは異なるイミュータブル(不変)なデータ構造を簡単に表現できる便利な機能です。

この記事では、record型の基本的な使い方からclass型との違い、便利な機能、実際の活用シーンまで、実務でよく使うポイントを丁寧に解説します。

C#のrecord型(レコード型)とは?

record は、データ型の一つで、見た目や書き方はクラスによく似ています。

レコードもクラスと同じように、プロパティやメソッドを定義したり、継承やインターフェースを使うことができます。

まずは、クラスと同じような書き方で定義する例を見てみましょう。

public record User
{
    public string Name { get; init; }
    public int Age { get; init; }

    public string Greet()
    {
        return $"Hello, {Name}!";
    }
}

record キーワード以外はクラスと見た目は同じです。
※プロパティのinit アクセサについては後ほど説明します。

レコードには「プライマリコンストラクタ」と呼ばれる省略記法があり、プロパティだけを持つシンプルな構造の場合は、1行で書くことができます。

public record User(string Name, int Age);

この一行で、NameAge という2つのプロパティやコンストラクタが自動的に定義されます。

インスタンス生成も通常のクラスと同様です。(ただし必ず全プロパティの初期化が必要)

var user = new User("Alice", 30);
Console.WriteLine(user); // 出力: User { Name = Alice, Age = 30 }

この省略スタイルは、主にデータの中身だけを扱いたい場面で使われることが多く、表示用のモデルや設定情報などに最適です。

レコードとクラスの違い

先ほどのプライマリコンストラクタによる省略記法以外にも、レコードとクラスでは異なる点があります。

比較方法が異なる

レコードとクラスでは、「インスタンス同士の比較方法」が異なります。

クラスでは、インスタンス同士を==Equalsメソッドで比較すると「中身が同じであっても、異なるインスタンスは別物」として判定されます。

これはクラスは参照型であり、「データそのもの」ではなく「データが置かれている場所(アドレス)」を比べているからです。

・値型(int, double, bool など) は、変数そのものにデータ(値)を持つため、比較するときは値同士を直接比べます。
・参照型(string, List<T> など) は、変数にデータそのものではなく「データがある場所(アドレス)」を持つため、通常の比較では「同じ場所を参照しているかどうか」が基準になります。

一方、レコードも参照型ですが、「中身の値がすべて同じなら、同じものとみなす」というように、値ベースの比較を行うように設計されています。

以下はクラスとレコードのインスタンス同士を比較する例です。

// クラスの比較例
public class UserClass
{
    public string Name { get; set; }
}

var c1 = new UserClass { Name = "Bob" };
var c2 = new UserClass { Name = "Bob" };

Console.WriteLine(c1 == c2); // false(中身が同じでも参照が異なるため)


// レコードの比較例
public record UserRecord(string Name);

var r1 = new UserRecord("Bob");
var r2 = new UserRecord("Bob");

Console.WriteLine(r1 == r2); // true(中身が同じのため)

このように、クラスは参照が同じかどうか、レコードは中身が同じかどうかで判定します。

レコードは一部機能が自動実装される

レコードには、クラスでは手動で実装しなければならない便利な機能が自動で組み込まれています

代表的なのは、ToString(), Equals(), GetHashCode() などのメソッドです。

これらのメソッドは、クラスでは自分で定義する必要がありますが、レコードでは何もしなくても最初から使えるようになっています。

以下は、クラスとレコードでToString()メソッドの違いを表したサンプルコードです。

// クラスの定義(ToString()を定義していない)
public class UserClass
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// レコードの定義(ToString()が自動で使える)
public record UserRecord(string Name, int Age);

var classUser = new UserClass { Name = "Alice", Age = 30 };
var recordUser = new UserRecord("Alice", 30);

// Console.WriteLineにオブジェクトを渡すと自動でToString()が呼ばれる
Console.WriteLine(classUser);  // 出力: UserClass (中身は表示されない)
Console.WriteLine(recordUser); // 出力: UserRecord { Name = Alice, Age = 30 }

このように、クラスでToString()メソッドを定義していない場合はクラス名がそのまま表示されますが、レコードは定義していなくても値の中身が表示されます。

レコードはイミュータブル(不変)な性質を持つ

「一度作ったら中身を変更できない」データのことをイミュータブル(不変)と呼び、その性質を持っているのもレコードの大きな特徴です。

クラスはミュータブル(可変)なので、生成したインスタンスのプロパティの値を自由に変更できますが、レコードはイミュータブルなのでインスタンス生成時以外は値を変更することができません。

public record Person(string Name, int Age);

var person1 = new Person("Alice", 30);
person1.Age = 31 // エラー。インスタンス生成後に値を変更することはできない
var person2 = new Person("Alice", 31); // 値が違うものは別の新しいインスタンスを生成する必要がある

Console.WriteLine(person1); // Person { Name = Alice, Age = 30 }
Console.WriteLine(person2); // Person { Name = Alice, Age = 31 }

このようにレコードでは元のデータを変更するのではなく、必要に応じて新しいインスタンスを作ることで、イミュータブルな設計を実現します。

イミュータブルのメリット

イミュータブルの一度作ったら中身が変わらないという特徴は、「予期しないバグ」を防いだり、「安心してデータを共有できる」ようになるという大きなメリットがあります。

public class Person
{
    public string Name { get; set; }
}

void Greet(Person person)
{
    person.Name = "Bob";  // 呼び出し元のオブジェクトを変更してしまう
    Console.WriteLine($"Hello, {person.Name}!");
}

var p = new Person { Name = "Alice" };
Greet(p);
Console.WriteLine(p.Name);  // Bob になっている(意図しない変更)

この例では、GreetメソッドがPersonNameを書き換えており、呼び出し元のデータが変わってしまっています。

意図しない場所でデータが変更されると、後からバグの原因を特定するのが難しくなります

一方でレコードは、インスタンス生成後に値を変更できなくなるのでこのような心配がなくなります。

一人で開発するアプリケーションであればあまり気にしなくても大丈夫ですが、複数人で開発する複雑なアプリケーションではイミュータブルな設計が重要になってきます。

with式

「一度作ったら変更できない」イミュータブルの仕組みは安全性の面で非常に有効ですが、
現実の開発では「一部の値だけを変えたい」と思う場面もよくあります。

そんなときに役立つのがwith式です。

with式を使えば、元のインスタンスを変更せずに、一部のプロパティだけ変えた新しいインスタンスを簡単に作ることができます。

var original = new User("Alice", 30);
var updated = original with { Age = 31 };

Console.WriteLine(original); // User { Name = Alice, Age = 30 }
Console.WriteLine(updated);  // User { Name = Alice, Age = 31 }

このように、元のオブジェクトはそのままに、必要な部分だけを変更した新しいオブジェクトを簡単に作ることができます。

initアクセサ

C# 9.0以降で導入されたinitアクセサは、プロパティの値を「オブジェクトの初期化時だけ設定できる」ように制限するための仕組みです。

これにより、インスタンスを作った後にプロパティが変更されることを防げます。

イミュータブルな設計と非常に相性が良く、「一度値を決めたら、あとから変えられない」という安心なデータモデルを作ることができます。

public record Product
{
    public string Name { get; init; }
    public int Price { get; init; }
}

var p = new Product { Name = "Note", Price = 500 };
// p.Price = 600; // エラー。init プロパティは初期化後に変更できない

このように、initアクセサを使うことで、初期化時のみプロパティに値を設定でき、その後は変更できなくなります

先ほど紹介したように、プライマリコンストラクタを使った場合は、自動的にinitアクセサが適用されたプロパティになります。

public record Product(string Name, int Price);

var p = new Product("Note", 500);
// p.Price = 600; // エラー。init プロパティは初期化後に変更できない

プライマリコンストラクタを使うと、public string Name { get; init; } のようなプロパティが自動で生成されるイメージです。

クラスでもinitアクセサは利用できますが、レコードと組み合わせることで「イミュータブル+シンプルな定義」が簡単に実現できます。

レコードの使いどころ

C#のrecord型(レコード型)は、「データの中身が同じなら同じものとみなしたい」場面で特に便利なデータ構造です。

実務では、次のような用途でレコード型がよく使われています。

DTO(データ転送オブジェクト)

DTO(Data Transfer Object)とは、システム内部や外部サービスとの間でデータを受け渡すための「データ専用の入れ物」です。

DTOの主な役割は、「データそのものを運ぶこと」なので、ビジネスロジックや振る舞い(メソッド)を持たせず、純粋にプロパティだけを持つ設計にします。

レコード型は、DTOのような「中身の値が重要なデータ構造」をシンプルかつイミュータブルに表現できるため、DTOの実装方法として非常に相性が良いのが特長です。

public record UserDto(string Name, int Age);

var dto1 = new UserDto("Alice", 30);
var dto2 = new UserDto("Alice", 30);

Console.WriteLine(dto1 == dto2); // True(中身が同じなら等しいと判定される)

このように、単にデータだけを保持したい場合はレコード型を使えばシンプルかつ安全に実装できます。

APIリクエスト・レスポンスモデル

レコード型は、Web APIのリクエストやレスポンスのデータモデルとしてよく使われます。

イミュータブルで比較やコピーも簡単なため、データの安全なやりとりやシンプルな記述が可能です。

public record CreateUserRequest(string Name, int Age);
public record CreateUserResponse(int Id, string Name, int Age);

System.Text.Jsonを使えば、レコードのインスタンスのJSON文字列への変換や、JSONからのデータ復元を簡単に行うことができます。

using System.Text.Json;

var req = new CreateUserRequest("Alice", 30);
string json = JsonSerializer.Serialize(req); // JSONへ変換
var req2 = JsonSerializer.Deserialize<CreateUserRequest>(json); // JSONから復元

System.Text.Jsonの詳しい使い方については以下の記事を参照してください。
【C#】System.Text.Jsonの基本的な使い方|hiranote

値オブジェクト(ValueObject)

レコード型は、「値オブジェクト」を表現するのに特に適しています。

値オブジェクトとは、「そのデータの中身(値)が等しければ、同じものとみなす性質を持つオブジェクト」です。

例えば、郵便番号やメールアドレス、金額や氏名などは、値が同じであれば“同じもの”として扱えます。

レコード型を使うことで、こうした値オブジェクトをシンプルかつ安全に定義できます。

public record PostalCode(string Value);
public record Email(string Value);
public record Money(decimal Amount, string Currency);

レコード型は「中身が同じなら等しい」と自動的に判定してくれるため、値オブジェクトを実装する際に余計なコードを書かなくて済むのが大きなメリットです。

(補足)record struct(レコード構造体)とは?

C# 10.0からは、「record struct(レコード構造体)」という新しいデータ構造も利用できるようになりました。

今まで登場したrecord(=record class)は「参照型」ですが、record structは「値型」としてより軽量に扱えるのが特徴です。

record structは、小さいデータや頻繁に作成・破棄される値のかたまり(たとえば座標やサイズなど)を表現したい場合に使われます。

public record struct Point(int X, int Y);

var p1 = new Point(10, 20);
var p2 = new Point(10, 20);

Console.WriteLine(p1 == p2); // true(中身が同じなら等しいと判定される)

なお、通常の業務アプリやWeb開発ではrecord structを自分で定義する機会はほぼありません。(パフォーマンスが重要な分野などでは使われることがあります)

まずはrecord classの使い方をしっかり理解するようにしましょう。

参考ドキュメント

C#のrecord型の仕様について、より詳しく知りたい方は以下の公式ドキュメントもご参照ください。

レコード - C# reference
C# のクラス型と構造体型のレコード修飾子について説明します。 レコードは、レコード型のインスタンスに対する値ベースの等価性を標準でサポートします。

まとめ

record型は、「値重視」のデータ設計や安全なデータの受け渡しにとても便利な新しいC#の機能です。

クラスと比べて、イミュータブルな設計・値ベースの比較・自動生成される便利な機能など、さまざまなメリットがあります。

実際の開発現場でも、DTOやAPIモデル、値オブジェクトなど、「中身が同じなら同じもの」と考えたい場面でrecord型が多く使われています。

今回紹介した違いや使いどころを理解し、自分のアプリケーションでもrecord型を積極的に活用してみてください。

次回は、C#における「null」の扱い方について解説します。

コメント

タイトルとURLをコピーしました