C#のrecordとは?classとの違い・使い方・注意点を初心者向けに徹底解説
はじめに
C#でデータを扱う型を作るとき、よく使われるのがclassです。しかし、C#にはrecordという型も用意されています。
recordは、ユーザー情報、設定値、APIレスポンス、DTO、Value Objectなど、「データそのもの」を表現したい場面で便利な機能です。特に、同じ値を持つオブジェクトを「同じもの」として比較したい場合や、変更に強いイミュータブルな設計をしたい場合に役立ちます。
一方で、recordはclassの完全な代替ではありません。仕組みを理解せずに使うと、思わぬ比較結果になったり、参照型プロパティの扱いでバグを生んだりすることもあります。
この記事では、C#のrecordとは何か、classとの違い、基本的な書き方、便利な使い方、注意点、使い分けまでを初心者向けにわかりやすく解説します。
1. C#のrecordとは?初心者向けにわかりやすく解説
C#のrecordは、データを表現するために便利な型です。
通常のclassでもデータを持つ型は作れますが、recordを使うと、値の比較、文字列表現、コピーして一部だけ変更する処理などを、少ないコードで実装できます。
たとえば、名前と年齢を持つユーザー情報を表す場合、recordなら次のように短く書けます。
C#public record User(string Name, int Age);
これだけで、NameとAgeを持つ型が作られます。さらに、値ベースの比較やToString()、with式なども利用できます。
1-1. recordは「値の同一性」を扱いやすくする型
recordの大きな特徴は、「値の同一性」を扱いやすいことです。
値の同一性とは、別々に作られたインスタンスであっても、中身の値が同じなら同じものとして扱う考え方です。
たとえば、次のようなrecordがあるとします。
C#public record User(string Name, int Age);
var user1 = new User("Taro", 20);
var user2 = new User("Taro", 20);
Console.WriteLine(user1 == user2); // True
user1とuser2は別々にnewされています。しかし、NameとAgeの値が同じなので、recordではtrueになります。
通常のclassでは、何も実装しなければ参照が同じかどうかで比較されます。そのため、同じ値を持っていても別のインスタンスならfalseになります。
つまり、recordは「オブジェクトの中身が同じかどうか」を自然に扱いたいときに向いています。
1-2. C# 9.0で追加されたrecordの基本
recordはC# 9.0で追加された機能です。もともとは参照型のrecord classとして導入され、その後のC#ではrecord structも使えるようになりました。
現在のC#では、主に次のような書き方ができます。
C#public record Person(string Name, int Age);
C#public record class Person(string Name, int Age);
C#public record struct Point(int X, int Y);
recordまたはrecord classは参照型、record structは値型です。recordとだけ書いた場合は、基本的にrecord classとして扱われます。
Microsoftの公式ドキュメントでも、recordは値ベースの等価性、ToString()、with式、位置指定構文などを備えたデータ向けの型として説明されています。Microsoft Learn+1
1-3. class・struct・recordの位置づけ
C#には、データや振る舞いを表すための代表的な型としてclass、struct、recordがあります。
classは参照型です。オブジェクトとしての状態や振る舞いを持ち、業務ロジックを含むような型に向いています。
structは値型です。小さくて軽量な値を表すときに使われます。たとえば、座標、日付、数値の組み合わせなどです。
recordは、データ中心の型を簡潔に書くための仕組みです。値ベースの比較を標準で持っているため、DTOや設定値、Value Objectのような用途に向いています。
ざっくり言うと、次のように考えるとわかりやすいです。
class : オブジェクトの状態や振る舞いを表す
struct : 小さな値を表す
record : データそのものを表す
もちろん、実際の設計では例外もあります。しかし、初心者のうちは「データ中心ならrecord」「振る舞い中心ならclass」と考えると判断しやすくなります。
1-4. recordが使われる主な場面
C#のrecordは、次のような場面でよく使われます。
C#public record UserDto(int Id, string Name, string Email);
このようなDTOは、データを運ぶことが主な目的です。複雑な状態変更や振る舞いを持たないため、recordと相性が良いです。
APIレスポンスの受け取りにも使えます。
C#public record ProductResponse(int Id, string Name, decimal Price);
設定値の表現にも向いています。
C#public record AppSettings(string ConnectionString, int TimeoutSeconds);
また、ドメイン駆動設計で使われるValue Objectにも適しています。
C#public record EmailAddress(string Value);
このように、recordは「値として意味を持つデータ」を表すときに便利です。
2. C#のrecordとclassの違い
recordとclassは似ています。どちらもプロパティやメソッドを持てますし、インスタンスを作ることもできます。
しかし、重要な違いがあります。特に初心者が押さえておきたいのは、比較方法、イミュータブル性、ToString()、with式です。
2-1. 比較方法の違い:参照比較と値比較
classとrecordの最も大きな違いは、比較方法です。
通常のclassでは、==で比較すると参照が同じかどうかを見ます。
C#public class UserClass
{
public string Name { get; set; }
public int Age { get; set; }
}
var user1 = new UserClass { Name = "Taro", Age = 20 };
var user2 = new UserClass { Name = "Taro", Age = 20 };
Console.WriteLine(user1 == user2); // False
user1とuser2は同じ値を持っていますが、別々のインスタンスなのでfalseになります。
一方、recordでは値が同じならtrueになります。
C#public record UserRecord(string Name, int Age);
var user1 = new UserRecord("Taro", 20);
var user2 = new UserRecord("Taro", 20);
Console.WriteLine(user1 == user2); // True
recordは、型と各プロパティ・フィールドの値が一致していれば等しいものとして扱います。これはrecordの大きなメリットです。Microsoft Learn+1
2-2. イミュータブルなデータを作りやすい
recordは、イミュータブルなデータを作りやすい構文を持っています。
イミュータブルとは、作成後に状態を変更しないことです。
たとえば、次のrecordでは、プライマリコンストラクタで定義したプロパティは基本的にinit専用のプロパティとして扱われます。
C#public record User(string Name, int Age);
作成時には値を設定できます。
C#var user = new User("Taro", 20);
しかし、作成後に次のように変更することはできません。
C#// user.Name = "Jiro"; // コンパイルエラー
このように、recordは不用意な変更を防ぎやすい型です。
ただし、recordだから必ず完全にイミュータブルになるわけではありません。後述するように、可変プロパティや参照型プロパティを持たせると変更可能な状態を含むことがあります。
2-3. ToString()の出力がわかりやすい
recordでは、ToString()の出力もわかりやすくなります。
通常のclassでToString()を呼ぶと、型名だけが表示されることが多いです。
C#public class UserClass
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserClass { Name = "Taro", Age = 20 };
Console.WriteLine(user.ToString());
出力例は次のようになります。
UserClass
一方、recordではプロパティ名と値が含まれます。
C#public record UserRecord(string Name, int Age);
var user = new UserRecord("Taro", 20);
Console.WriteLine(user.ToString());
出力例は次のようになります。
UserRecord { Name = Taro, Age = 20 }
デバッグ時に中身を確認しやすいのは、recordの大きな利点です。
2-4. with式でコピーしながら一部だけ変更できる
recordでは、with式を使って既存の値をコピーし、一部だけ変更した新しいインスタンスを作れます。
C#public record User(string Name, int Age);
var user1 = new User("Taro", 20);
var user2 = user1 with { Age = 21 };
Console.WriteLine(user1); // User { Name = Taro, Age = 20 }
Console.WriteLine(user2); // User { Name = Taro, Age = 21 }
user1は変更されません。Ageだけが変わった新しいuser2が作られます。
これは「非破壊的変更」と呼ばれます。元のデータを壊さずに、新しいデータを作る考え方です。
状態変更によるバグを減らしたい場合に便利です。
2-5. 継承・コンストラクタ・プロパティの違い
record classは参照型なので、classと同じように継承できます。
C#public record Person(string Name);
public record Employee(string Name, int EmployeeId) : Person(Name);
ただし、recordの継承では値比較の仕組みも関わるため、通常のclassより注意が必要です。
また、recordにはプライマリコンストラクタを使った短い書き方があります。
C#public record Product(int Id, string Name, decimal Price);
これは、次のようなデータ中心の型を簡潔に書きたいときに便利です。
C#public record Product
{
public int Id { get; init; }
public string Name { get; init; }
public decimal Price { get; init; }
}
classでも似たことはできますが、値比較やwith式、ToString()などを自分で実装する必要があります。
2-6. recordとclassの違いを表で比較
| 項目 | class | record |
|---|---|---|
| 基本的な用途 | 状態や振る舞いを持つオブジェクト | データ中心の型 |
| デフォルトの比較 | 参照比較 | 値比較 |
==の挙動 | 同じ参照かどうか | 値が同じかどうか |
ToString() | 型名だけになりやすい | プロパティ名と値が出力される |
with式 | 基本的に使えない | 使える |
| イミュータブル設計 | 自分で設計する必要がある | 作りやすい |
| DTOとの相性 | 悪くない | 良い |
| 複雑な状態管理 | 向いている | 注意が必要 |
初心者向けにまとめると、データを運ぶだけならrecord、状態を変更しながら振る舞いを持つならclassが向いています。
3. C# recordの基本的な書き方
ここからは、C#のrecordの基本的な書き方を見ていきましょう。
recordには、短く書ける方法と、プロパティを明示して書く方法があります。
3-1. 最もシンプルなrecordの定義
最もシンプルなrecordは次のように書きます。
C#public record User(string Name, int Age);
この1行で、NameとAgeを持つUser型を定義できます。
使うときは次のようにインスタンス化します。
C#var user = new User("Taro", 20);
Console.WriteLine(user.Name); // Taro
Console.WriteLine(user.Age); // 20
このような書き方を「位置指定レコード」または「positional record」と呼ぶことがあります。
3-2. プライマリコンストラクタを使った書き方
recordでは、型名の後ろに引数を書くことで、プライマリコンストラクタを定義できます。
C#public record Product(int Id, string Name, decimal Price);
これは、次のように使えます。
C#var product = new Product(1, "Keyboard", 5000m);
Console.WriteLine(product.Id);
Console.WriteLine(product.Name);
Console.WriteLine(product.Price);
データ項目が少なく、単純なDTOやレスポンス型を作る場合は、この書き方がとても便利です。
ただし、引数の順番が意味を持つため、項目が多すぎると読みづらくなります。その場合は、プロパティを明示して書く方法も検討しましょう。
3-3. プロパティを明示して定義する書き方
recordは、通常のclassのようにプロパティを明示して書くこともできます。
C#public record User
{
public int Id { get; init; }
public string Name { get; init; } = "";
public string Email { get; init; } = "";
}
この書き方では、オブジェクト初期化子を使って値を設定できます。
C#var user = new User
{
Id = 1,
Name = "Taro",
Email = "taro@example.com"
};
initを使うと、初期化時には値を設定できますが、初期化後の変更は制限されます。
C#// user.Name = "Jiro"; // コンパイルエラー
initアクセサは、オブジェクト構築時のみプロパティに値を設定できる仕組みです。初期化後の変更を防ぐため、イミュータブルな設計でよく使われます。Microsoft Learn+1
3-4. record classとrecord structの違い
recordには、record classとrecord structがあります。
C#public record class User(string Name, int Age);
C#public record struct Point(int X, int Y);
record classは参照型です。通常のclassと同じように、変数にはオブジェクトへの参照が入ります。
一方、record structは値型です。通常のstructと同じように、値そのものとして扱われます。
初心者のうちは、まずrecordまたはrecord classを使うケースが多いでしょう。record structは、座標や範囲、軽量な値の組み合わせなど、値型として扱いたい場合に検討します。
3-5. readonly record structとは
readonly record structは、読み取り専用のrecord structです。
C#public readonly record struct Point(int X, int Y);
readonlyを付けることで、その構造体の状態を変更しないことを明示できます。
たとえば、座標やサイズのように、一度作ったら変更しない小さな値を表すときに向いています。
C#public readonly record struct Size(int Width, int Height);
ただし、structは値型特有のコピーやボックス化などを理解して使う必要があります。初心者が無理にrecord structやreadonly record structを選ぶ必要はありません。
まずはrecord classを理解し、必要になったらrecord structを学ぶとよいでしょう。
4. C# recordの便利な機能と使い方
C#のrecordには、データ中心の型を扱いやすくする便利な機能があります。
ここでは、よく使う機能を順番に紹介します。
4-1. 値ベースのEquals比較
recordでは、Equals()も値ベースで比較されます。
C#public record User(string Name, int Age);
var user1 = new User("Taro", 20);
var user2 = new User("Taro", 20);
Console.WriteLine(user1.Equals(user2)); // True
通常のclassでは、Equals()をオーバーライドしない限り、参照比較になることが多いです。
一方、recordでは、プロパティの値が同じなら等しいと判断されます。
これにより、リストの検索や重複チェックなどでも扱いやすくなります。
C#var users = new List<User>
{
new User("Taro", 20),
new User("Jiro", 25)
};
Console.WriteLine(users.Contains(new User("Taro", 20))); // True
recordでなければ、このような値ベースの比較を自分で実装する必要があります。
4-2. ==演算子による比較
recordでは、==演算子も値ベースで比較できます。
C#public record Product(int Id, string Name);
var product1 = new Product(1, "Mouse");
var product2 = new Product(1, "Mouse");
Console.WriteLine(product1 == product2); // True
これは初心者にとって直感的です。
「同じIDで同じ名前なら同じ商品データ」と考えたい場合、recordは自然な書き方になります。
ただし、recordの比較は、基本的に定義された値をもとに行われます。どの値を比較対象にしたいかを意識して設計することが大切です。
4-3. with式による非破壊的変更
with式は、recordの代表的な便利機能です。
C#public record User(string Name, int Age);
var before = new User("Taro", 20);
var after = before with { Age = 21 };
このコードでは、beforeを変更せずに、Ageだけを変更したafterを作っています。
C#Console.WriteLine(before); // User { Name = Taro, Age = 20 }
Console.WriteLine(after); // User { Name = Taro, Age = 21 }
これにより、元のデータを安全に残しながら、新しい状態を作ることができます。
たとえば、ユーザー情報の一部だけを更新する場合にも便利です。
C#var updatedUser = user with
{
Email = "new@example.com"
};
変更前のデータを壊さないため、バグを減らしやすくなります。
4-4. initアクセサによる初期化後の変更制限
initアクセサを使うと、オブジェクトの初期化時だけ値を設定できます。
C#public record User
{
public string Name { get; init; } = "";
public int Age { get; init; }
}
次のように初期化できます。
C#var user = new User
{
Name = "Taro",
Age = 20
};
しかし、作成後に値を変更することはできません。
C#// user.Age = 21; // コンパイルエラー
これにより、意図しないデータ変更を防げます。
特に、複数の場所で同じデータを参照するアプリケーションでは、どこかで勝手に値が変わるとバグの原因になります。initを使ったrecordは、そのような問題を減らすのに役立ちます。
4-5. Deconstructによる分解
位置指定recordでは、Deconstructも自動的に使えます。
C#public record User(string Name, int Age);
var user = new User("Taro", 20);
var (name, age) = user;
Console.WriteLine(name); // Taro
Console.WriteLine(age); // 20
このように、recordの値を変数に分解できます。
メソッドの戻り値を受け取ったり、一部の値だけを使いたいときに便利です。
C#var (_, userAge) = user;
Console.WriteLine(userAge);
_を使うと、不要な値を無視できます。
4-6. ToString()の自動生成
recordでは、ToString()が自動生成されます。
C#public record Order(int Id, decimal TotalAmount);
var order = new Order(100, 9800m);
Console.WriteLine(order);
出力例は次のようになります。
Order { Id = 100, TotalAmount = 9800 }
通常のclassでは、わかりやすい文字列を出すためにToString()を自分でオーバーライドする必要があります。
C#public override string ToString()
{
return $"Order {{ Id = {Id}, TotalAmount = {TotalAmount} }}";
}
recordなら、このようなコードを書かなくても、デバッグしやすい文字列を得られます。
5. recordを使った具体的なサンプルコード
ここでは、実際にrecordを使う場面を想定して、具体的なサンプルコードを紹介します。
5-1. ユーザー情報をrecordで表現する例
ユーザー情報のようなデータは、recordで表現しやすい代表例です。
C#public record User(
int Id,
string Name,
string Email
);
使い方は次のとおりです。
C#var user = new User(
Id: 1,
Name: "Taro",
Email: "taro@example.com"
);
Console.WriteLine(user.Name);
Console.WriteLine(user.Email);
名前付き引数を使うと、どの値を渡しているかがわかりやすくなります。
一部だけ変更したい場合は、with式を使います。
C#var updatedUser = user with
{
Email = "new-taro@example.com"
};
Console.WriteLine(updatedUser);
元のuserは変更されず、新しいupdatedUserが作られます。
5-2. DTOをrecordで作る例
DTOは、Data Transfer Objectの略で、データを受け渡しするためのオブジェクトです。
DTOは基本的にデータを運ぶことが目的なので、recordと相性が良いです。
C#public record UserDto(
int Id,
string Name,
string Email
);
たとえば、サービス層から画面側へデータを返すときに使えます。
C#public UserDto GetUser()
{
return new UserDto(
Id: 1,
Name: "Taro",
Email: "taro@example.com"
);
}
DTOには複雑な振る舞いを持たせないことが多いため、短く書けるrecordは便利です。
5-3. APIレスポンスをrecordで受け取る例
Web APIのレスポンスを受け取る型にもrecordを使えます。
C#public record ProductResponse(
int Id,
string Name,
decimal Price
);
たとえば、JSONからデシリアライズして使う場合を考えます。
C#var product = new ProductResponse(
Id: 10,
Name: "Keyboard",
Price: 5000m
);
Console.WriteLine(product);
レスポンスデータは「受け取った値を表す」ことが主な役割です。そのため、値中心のrecordに向いています。
ただし、JSONシリアライズやデシリアライズで使う場合は、使用するライブラリの仕様に合わせてコンストラクタやプロパティの定義を確認しましょう。
5-4. Value Objectとしてrecordを使う例
Value Objectは、IDではなく値そのものによって同一性を判断するオブジェクトです。
たとえば、メールアドレスを表す型を考えてみます。
C#public record EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("メールアドレスは必須です。", nameof(value));
}
Value = value;
}
}
使い方は次のとおりです。
C#var email1 = new EmailAddress("taro@example.com");
var email2 = new EmailAddress("taro@example.com");
Console.WriteLine(email1 == email2); // True
値が同じなら同じものとして扱えるため、Value Objectとrecordは相性が良いです。
さらに、値の検証ロジックをコンストラクタに入れることで、不正な値を持つオブジェクトを作らせない設計にできます。
5-5. classで書いたコードをrecordに置き換える例
次のようなclassがあるとします。
C#public class UserClass
{
public int Id { get; }
public string Name { get; }
public string Email { get; }
public UserClass(int id, string name, string email)
{
Id = id;
Name = name;
Email = email;
}
}
この型は、データを持つだけで複雑な振る舞いはありません。この場合、recordに置き換えると短く書けます。
C#public record User(int Id, string Name, string Email);
これだけで、プロパティ、コンストラクタ、値比較、ToString()、with式などが使えます。
C#var user1 = new User(1, "Taro", "taro@example.com");
var user2 = new User(1, "Taro", "taro@example.com");
Console.WriteLine(user1 == user2); // True
Console.WriteLine(user1);
データ中心のclassを見つけたら、recordにできないか検討してみるとよいでしょう。
6. C# recordを使うメリット
C#のrecordには、多くのメリットがあります。特に、データ中心の型を扱う場面ではコード量を減らし、意図を明確にできます。
6-1. データ中心の型を短く書ける
recordを使う最大のメリットのひとつは、データ中心の型を短く書けることです。
たとえば、classで次のように書く型を考えます。
C#public class Product
{
public int Id { get; }
public string Name { get; }
public decimal Price { get; }
public Product(int id, string name, decimal price)
{
Id = id;
Name = name;
Price = price;
}
}
recordなら次の1行で書けます。
C#public record Product(int Id, string Name, decimal Price);
コードが短くなるだけでなく、「これはデータを表す型である」という意図も伝わりやすくなります。
6-2. 値比較を簡単に実装できる
classで値比較を正しく実装するには、Equals()やGetHashCode()をオーバーライドする必要があります。
C#public override bool Equals(object? obj)
{
// 比較処理を書く
}
public override int GetHashCode()
{
// ハッシュコードを返す
}
この実装は面倒で、ミスも起きやすいです。
recordなら、値ベースの比較が自動的に利用できます。
C#public record User(int Id, string Name);
var user1 = new User(1, "Taro");
var user2 = new User(1, "Taro");
Console.WriteLine(user1 == user2); // True
データの重複チェックやコレクション操作で便利です。
6-3. イミュータブル設計に向いている
recordは、イミュータブル設計と相性が良いです。
C#public record User(string Name, int Age);
このように定義すると、作成後にプロパティを簡単には変更できません。
値を変えたい場合は、with式で新しいインスタンスを作ります。
C#var user = new User("Taro", 20);
var updated = user with { Age = 21 };
元のオブジェクトを変更しないため、状態変更によるバグを防ぎやすくなります。
6-4. バグの少ないデータ更新がしやすい
可変なオブジェクトでは、どこかの処理で値が変更されると、別の処理にも影響が出ることがあります。
C#user.Name = "Jiro";
このような変更が多いと、いつ値が変わったのか追いにくくなります。
recordでは、変更したいときに新しい値を作る設計にしやすくなります。
C#var newUser = user with { Name = "Jiro" };
この書き方なら、元の値と新しい値が明確に分かれます。
履歴を残したい処理や、状態の変更を追跡したい処理では特に便利です。
6-5. DTOや設定値の表現に向いている
recordは、DTOや設定値のように、データを保持することが主な目的の型に向いています。
C#public record DatabaseSettings(
string Host,
int Port,
string DatabaseName
);
設定値は基本的に、アプリケーション起動時に読み込んだら途中で変更しないことが多いです。
そのため、recordで表現すると意図が伝わりやすくなります。
C#var settings = new DatabaseSettings(
Host: "localhost",
Port: 5432,
DatabaseName: "sample_db"
);
データを安全に扱いたい場合、recordは有力な選択肢です。
7. C# recordを使うときの注意点
recordは便利ですが、万能ではありません。注意点を理解しないまま使うと、思わぬバグにつながることがあります。
7-1. 完全な不変性が自動で保証されるわけではない
recordはイミュータブルな設計をしやすい型ですが、完全な不変性を自動で保証するわけではありません。
たとえば、次のようにsetを使うこともできます。
C#public record User
{
public string Name { get; set; } = "";
}
この場合、作成後でも値を変更できます。
C#var user = new User { Name = "Taro" };
user.Name = "Jiro";
これでは、recordのイミュータブルなメリットは弱くなります。
また、initを使っていても、プロパティの中身が可変な参照型なら注意が必要です。
7-2. 参照型プロパティは浅いコピーになる
with式でコピーするとき、参照型プロパティは基本的に浅いコピーになります。
浅いコピーとは、参照先のオブジェクトまでは複製せず、同じ参照を共有するコピーです。
C#public record User(string Name, List<string> Tags);
var user1 = new User("Taro", new List<string> { "Admin" });
var user2 = user1 with { Name = "Jiro" };
user2.Tags.Add("Editor");
Console.WriteLine(string.Join(", ", user1.Tags)); // Admin, Editor
Console.WriteLine(string.Join(", ", user2.Tags)); // Admin, Editor
user2のTagsを変更したつもりでも、user1にも影響しています。
これは、Tagsが同じList<string>を参照しているためです。
この問題を避けるには、可変なコレクションをそのまま持たせない、読み取り専用コレクションを使う、必要に応じて深いコピーを行うなどの工夫が必要です。
7-3. 可変プロパティを持たせるとrecordのメリットが薄れる
recordに可変プロパティを持たせることはできます。
C#public record User
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
しかし、これではrecordのメリットである「値として安全に扱う」「変更を制限する」という特徴が弱くなります。
可変にする必要があるなら、そもそもclassの方が適している場合もあります。
recordを使うなら、基本的には次のような設計を意識しましょう。
C#public record User
{
public string Name { get; init; } = "";
public int Age { get; init; }
}
または、短い構文を使います。
C#public record User(string Name, int Age);
7-4. Entity Framework Coreのエンティティには注意が必要
Entity Framework Coreのエンティティにrecordを使う場合は注意が必要です。
EF Coreのエンティティは、データベース上の行を表し、IDによって同一性を判断することが多いです。一方、recordは値ベースの同一性を持ちます。
この考え方の違いにより、エンティティとしては扱いにくい場面があります。
たとえば、次のようなエンティティは通常classで表すことが多いです。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
エンティティはライフサイクルを持ち、状態が変化することがあります。そのため、値ベースのrecordよりもclassの方が自然な場合があります。
ただし、EF Coreでrecordを絶対に使えないわけではありません。設計方針やマッピング方法によっては使える場合もありますが、初心者はまずclassを選ぶ方が安全です。
7-5. 継承を多用する設計には向かない場合がある
record classは継承できます。
C#public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
しかし、recordの継承では、値比較や型の一致が関わります。継承階層が複雑になると、比較の意味がわかりにくくなることがあります。
データ中心の型としてシンプルに使うなら問題ありませんが、継承を多用するオブジェクト指向設計では、通常のclassの方が適している場合があります。
recordは「データの形を表す」用途に向いています。複雑な振る舞いや状態遷移を持つ型には慎重に使いましょう。
7-6. recordを使わないほうがよいケース
次のようなケースでは、recordを使わない方がよい場合があります。
・状態が頻繁に変わるオブジェクト
・IDで同一性を判断するエンティティ
・複雑な振る舞いを持つドメインオブジェクト
・可変コレクションを多く持つ型
・継承階層が複雑な型
たとえば、ショッピングカートのように商品を追加したり削除したりするオブジェクトは、状態が変わりやすいです。
C#public class ShoppingCart
{
private readonly List<string> _items = new();
public void AddItem(string item)
{
_items.Add(item);
}
}
このような型は、recordよりclassの方が自然です。
recordは便利ですが、「データ中心かどうか」を基準に使うことが大切です。
8. record・class・structの使い分け
C#では、record、class、structを適切に使い分けることが重要です。
ここでは、初心者にもわかりやすい判断基準を紹介します。
8-1. recordを使うべきケース
recordを使うべきなのは、データ中心の型を作りたい場合です。
たとえば、次のような型です。
C#public record UserDto(int Id, string Name, string Email);
C#public record ProductResponse(int Id, string Name, decimal Price);
C#public record Money(decimal Amount, string Currency);
次の条件に当てはまるなら、recordを検討するとよいでしょう。
・値が同じなら同じものとして扱いたい
・作成後にあまり変更しない
・DTOやAPIレスポンスを表したい
・設定値を表したい
・Value Objectを作りたい
・ToStringやEqualsを自動生成したい
データの意味を明確にし、コードを短く保ちたいときにrecordは便利です。
8-2. classを使うべきケース
classを使うべきなのは、状態や振る舞いを持つオブジェクトを作りたい場合です。
たとえば、次のような型です。
C#public class BankAccount
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
Balance += amount;
}
public void Withdraw(decimal amount)
{
Balance -= amount;
}
}
この型は、単なるデータではありません。残高という状態を持ち、入金や出金という振る舞いを持っています。
次のような場合はclassが向いています。
・状態が変化する
・メソッドによる振る舞いが重要
・IDで同一性を判断する
・エンティティを表したい
・複雑なライフサイクルを持つ
業務ロジックを持つオブジェクトは、recordよりclassの方がわかりやすいことが多いです。
8-3. structを使うべきケース
structは値型です。小さくて単純な値を表すときに使います。
たとえば、座標やサイズのような型です。
C#public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
ただし、structはコピーの扱いやパフォーマンス上の注意点があります。大きなデータをstructにすると、コピーコストが増えることがあります。
初心者のうちは、明確な理由がない限り、無理にstructを使う必要はありません。
8-4. record structを使うべきケース
record structは、recordの便利機能を持った値型です。
C#public readonly record struct Point(int X, int Y);
次のようなケースで使いやすいです。
・小さな値を表したい
・値型として扱いたい
・値比較を簡単に使いたい
・ToStringやDeconstructを自動生成したい
たとえば、座標、範囲、サイズ、軽量なID型などに使えます。
C#public readonly record struct UserId(int Value);
ただし、値型の性質を理解していない場合は注意が必要です。迷ったら、まずはrecord classまたは通常のclassを選ぶとよいでしょう。
8-5. 初心者向けの判断基準
初心者向けに、ざっくりした判断基準をまとめると次のようになります。
| 使いたいもの | 選ぶ型 |
|---|---|
| DTO、APIレスポンス、設定値 | record |
| Value Object | record |
| 状態が変わる業務オブジェクト | class |
| IDで同一性を判断するエンティティ | class |
| 小さく軽量な値 | structまたはrecord struct |
| 値比較したい小さな値型 | record struct |
迷ったときは、次のように考えると判断しやすいです。
データそのものを表したい → record
振る舞いを持つオブジェクトを作りたい → class
小さな値型を作りたい → struct / record struct
最初から完璧に使い分ける必要はありません。実際にコードを書きながら、型の役割を意識して選ぶことが大切です。
9. C# recordに関するよくある質問
最後に、C#のrecordについて初心者が疑問に思いやすいポイントをQ&A形式でまとめます。
9-1. recordはclassの代わりにいつでも使える?
いいえ、recordはclassの代わりにいつでも使えるわけではありません。
recordはデータ中心の型に向いています。一方、状態が頻繁に変わるオブジェクトや、複雑な振る舞いを持つ型にはclassの方が向いています。
たとえば、次のようなDTOはrecordに向いています。
C#public record UserDto(int Id, string Name);
一方、次のような状態変更を持つ型はclassが向いています。
C#public class Counter
{
public int Value { get; private set; }
public void Increment()
{
Value++;
}
}
「データ中心ならrecord、振る舞い中心ならclass」と考えるとわかりやすいです。
9-2. recordはイミュータブルなの?
recordはイミュータブルな設計をしやすい型ですが、必ず完全にイミュータブルになるわけではありません。
たとえば、次のように書くと作成後に変更できます。
C#public record User
{
public string Name { get; set; } = "";
}
一方、次のようにinitを使うと、初期化後の変更を制限できます。
C#public record User
{
public string Name { get; init; } = "";
}
また、位置指定recordでは、初期化後に変更しにくいプロパティを簡単に作れます。
C#public record User(string Name);
ただし、プロパティにList<T>などの可変な参照型を持たせると、中身は変更できてしまうことがあります。
9-3. recordとDTOの相性は良い?
はい、recordとDTOの相性は良いです。
DTOはデータを受け渡すことが目的で、複雑な振る舞いを持たないことが多いためです。
C#public record UserDto(
int Id,
string Name,
string Email
);
recordを使うと、少ないコードでデータ構造を表現できます。
また、ToString()の出力がわかりやすいため、デバッグもしやすくなります。
ただし、シリアライザーやフレームワークによっては、引数なしコンストラクタやプロパティのsetが必要になる場合があります。使用するライブラリの仕様に合わせて定義しましょう。
9-4. recordでもメソッドは定義できる?
はい、recordでもメソッドを定義できます。
C#public record User(string FirstName, string LastName)
{
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
使い方は次のとおりです。
C#var user = new User("Taro", "Yamada");
Console.WriteLine(user.GetFullName());
recordはデータ中心の型ですが、必要に応じてメソッドを持たせることもできます。
ただし、複雑な状態変更を伴うメソッドが増えてきた場合は、classの方が適している可能性があります。
9-5. recordはパフォーマンスに影響する?
recordを使ったからといって、常に大きなパフォーマンス問題が起きるわけではありません。
ただし、値比較では複数のプロパティを比較するため、プロパティ数が多い場合や、比較対象に大きなオブジェクトが含まれる場合はコストがかかることがあります。
また、with式は新しいインスタンスを作るため、頻繁に大量のコピーを行う場合は注意が必要です。
通常のアプリケーション開発では、まず可読性や保守性を優先して問題ありません。パフォーマンスが重要な箇所では、実測して判断することが大切です。
9-6. record classとrecord structはどちらを選ぶべき?
迷った場合は、まずrecord classを選ぶのが無難です。
C#public record User(string Name, int Age);
record classは参照型で、通常のclassに近い感覚で使えます。DTOやAPIレスポンス、設定値など、多くの場面で使いやすいです。
一方、record structは値型です。
C#public readonly record struct Point(int X, int Y);
小さな値を大量に扱う場合や、値型としての意味が明確な場合に向いています。
初心者向けの判断基準は次のとおりです。
DTOやデータモデルを作りたい → record class
小さな値型を作りたい → record struct
よくわからない → まずはrecord class
record structは便利ですが、値型の性質を理解してから使うと安全です。
まとめ
C#のrecordは、データ中心の型を簡潔に書くための便利な機能です。
通常のclassと違い、recordは値ベースの比較を標準で持っています。そのため、同じ値を持つインスタンスを同じものとして扱いたい場合に向いています。
また、with式による非破壊的変更、initアクセサによる初期化後の変更制限、ToString()の自動生成、Deconstructなど、データを扱いやすくする機能も備えています。
一方で、recordは万能ではありません。完全な不変性が自動で保証されるわけではなく、参照型プロパティは浅いコピーになる点にも注意が必要です。状態が頻繁に変わるオブジェクトや、IDで同一性を判断するエンティティには、通常のclassの方が適している場合があります。
初心者のうちは、次のように使い分けるとよいでしょう。
データ中心の型、DTO、設定値、Value Object → record
状態や振る舞いを持つオブジェクト → class
小さく軽量な値型 → struct / record struct
C#のrecordを正しく理解すると、コードを短く書けるだけでなく、値の扱いが明確になり、保守しやすい設計につながります。データを表す型を作るときは、classだけでなくrecordも選択肢に入れてみましょう。

