C# recordとは?class・structとの違い、使いどころ、record class/structの選び方を初心者向けに解説
はじめに
C#のrecordは、DTO、APIレスポンス、設定値、値オブジェクトなど「データそのもの」を表す型を簡潔に書きたいときに便利な機能です。
ただし、recordを初めて見ると、次のような疑問が出やすいです。
「classと何が違うのか」
「structとはどう使い分けるのか」
「record classとrecord structはどちらを選べばよいのか」
「recordは不変オブジェクトなのか」
「業務アプリで使っても問題ないのか」
この記事では、C# recordの基本から、class・structとの違い、record class・record structの選び方、実践的な使いどころ、注意点まで初心者向けに解説します。
1. C# recordとは?初心者向けに一言で解説
C#のrecordとは、データを表す型を短く、安全に、読みやすく書くための仕組みです。
特に「ID、名前、金額、座標、設定値、APIレスポンス」のように、複数の値をひとまとめにして扱う型に向いています。
たとえば、ユーザー情報を表す型は次のように書けます。
C#public record User(int Id, string Name);
これだけで、IdとNameを持つ型を定義できます。さらに、等価比較、ToString()、分解、コピー作成など、データ型として便利な機能が自動で用意されます。
1-1. recordは「データを表す型」を簡潔に書くための仕組み
recordは、主に「データを運ぶ」「値の組み合わせを表す」目的で使います。
たとえば、次のような型に向いています。
C#public record ProductDto(int Id, string Name, decimal Price);
public record Address(string PostalCode, string Prefecture, string City);
public record SearchCondition(string Keyword, int Page, int PageSize);
従来のclassで同じような型を書くと、プロパティ、コンストラクター、Equals()、GetHashCode()、ToString()などを自分で書く必要がありました。recordを使うと、その多くをコンパイラーが自動生成してくれます。
Microsoft Learnでも、recordはデータをカプセル化するための組み込み機能を提供するものとして説明されています。recordまたはrecord classは参照型、record structは値型を定義します。
1-2. class・structと同じく型の一種だが、目的が違う
recordもclassやstructと同じく、C#で型を定義するための仕組みです。
ただし、目的が少し違います。
classは、状態と振る舞いを持つオブジェクトを表すのに向いています。たとえば、注文を確定する、在庫を減らす、メールを送る、といった処理を持つオブジェクトです。
structは、小さな値を効率よく扱う値型を表すのに向いています。たとえば、座標、日付、範囲、色、サイズなどです。
一方でrecordは、データのまとまりを表す型に向いています。データ中心の型なので、「中身の値が同じなら同じものとして扱いたい」という場面で特に便利です。
1-3. recordで自動生成される主な機能
recordを定義すると、主に次のような機能が自動で用意されます。
| 機能 | 内容 |
|---|---|
| 値ベースの等価性 | 中身の値が同じなら等しいと判定される |
ToString() | プロパティ名と値を含む見やすい文字列が出力される |
with式 | 一部の値だけ変えたコピーを作れる |
Deconstruct | 値を分解して変数に取り出せる |
GetHashCode() | 値に基づくハッシュコードが生成される |
| プライマリコンストラクター | 短い構文でプロパティとコンストラクターを書ける |
たとえば、次のコードではToString()を自分で書いていないのに、見やすい形式で出力されます。
C#public record User(int Id, string Name);
var user = new User(1, "Taro");
Console.WriteLine(user);
// User { Id = 1, Name = Taro }
Microsoft Learnでは、recordの代表的な機能として、値の等価性、非破壊的な変更のための簡潔な構文、表示用の組み込みフォーマットなどが挙げられています。
1-4. recordが登場した背景:DTOや値オブジェクトを楽に書きたい
recordが便利なのは、DTOや値オブジェクトのような「ほとんどデータだけを持つ型」を書くときです。
従来のclassでは、次のようなコードになりがちでした。
C#public class UserDto
{
public int Id { get; }
public string Name { get; }
public UserDto(int id, string name)
{
Id = id;
Name = name;
}
public override string ToString()
{
return $"UserDto {{ Id = {Id}, Name = {Name} }}";
}
}
これをrecordで書くと、次の1行で済みます。
C#public record UserDto(int Id, string Name);
もちろん、すべてのclassをrecordに置き換えるべきではありません。しかし、データを表すだけの型であれば、recordによってコード量を大きく減らせます。
2. C# recordの基本的な書き方
ここからは、C# recordの基本構文を見ていきます。
2-1. 最小構文:recordの定義例
もっともシンプルなrecordの例は次のとおりです。
C#public record User(int Id, string Name);
この定義だけで、IdとNameを持つUser型を作れます。
使うときは、通常の型と同じようにインスタンス化します。
C#var user = new User(1, "Taro");
Console.WriteLine(user.Id);
Console.WriteLine(user.Name);
出力結果は次のようになります。
1
Taro
このように、recordは短いコードでデータ用の型を作れるのが大きな特徴です。
2-2. プライマリコンストラクターを使った書き方
recordでは、型名の後ろに引数を書く構文をよく使います。
C#public record Product(int Id, string Name, decimal Price);
この(int Id, string Name, decimal Price)の部分は、プライマリコンストラクターのように使われます。
この構文を書くと、次のようなプロパティが自動生成されます。
C#public int Id { get; init; }
public string Name { get; init; }
public decimal Price { get; init; }
つまり、次のようにオブジェクトを作れます。
C#var product = new Product(1, "Keyboard", 9800m);
プライマリコンストラクターを使うと、DTOやAPIレスポンスの型を非常に短く表現できます。
Microsoft Learnでは、レコードにプライマリコンストラクターを宣言すると、コンパイラーがそのパラメーターに対応するパブリックプロパティを生成すると説明されています。
2-3. プロパティを明示的に書く方法
recordは、必ずしも1行で書く必要はありません。通常のclassのように、プロパティを明示的に書くこともできます。
C#public record User
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
この書き方は、次のような場合に便利です。
プロパティに初期値を設定したい場合。
C#public record SearchCondition
{
public string Keyword { get; init; } = "";
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 20;
}
プロパティごとにコメントを付けたい場合。
C#public record ProductDto
{
/// <summary>
/// 商品ID
/// </summary>
public int Id { get; init; }
/// <summary>
/// 商品名
/// </summary>
public string Name { get; init; } = "";
}
入力チェックや計算プロパティを追加したい場合。
C#public record Price(decimal Amount)
{
public bool IsFree => Amount == 0;
}
シンプルなデータ型なら1行の構文、少し複雑な型なら明示的なプロパティ、と使い分けると読みやすくなります。
2-4. recordは「record class」の省略形
C#では、次の2つはどちらも参照型のレコードを定義します。
C#public record User(int Id, string Name);
C#public record class User(int Id, string Name);
つまり、recordは基本的にrecord classの省略形です。
初心者のうちは、recordと書かれていたら「参照型のデータ用クラス」と考えると理解しやすいです。
ただし、record structは値型です。ここは混同しやすいので注意しましょう。
C#public record User(int Id, string Name); // 参照型
public record class Customer(int Id, string Name); // 参照型
public record struct Point(int X, int Y); // 値型
Microsoft Learnでも、recordまたはrecord classは参照型、record structは値型を宣言すると説明されています。
2-5. record struct / readonly record structの書き方
record structを使うと、値型のレコードを定義できます。
C#public record struct Point(int X, int Y);
使い方は通常の型と同じです。
C#var point = new Point(10, 20);
Console.WriteLine(point.X);
Console.WriteLine(point.Y);
readonly record structにすると、より変更不可に近い値型として扱えます。
C#public readonly record struct Point(int X, int Y);
record structは値型なので、record classとは代入やコピーの挙動が異なります。
初心者のうちは、まずrecord class、つまり通常のrecordを理解し、その後で必要に応じてrecord structを検討するとよいです。
3. recordの重要な特徴
recordを理解するうえで重要なのは、構文の短さだけではありません。recordには、データ中心の型として便利な特徴があります。
3-1. 値ベースの等価性:==やEqualsの動き
recordの大きな特徴は、値ベースの等価性です。
通常のclassでは、同じ値を持っていても、別々に作られたオブジェクトは別物として扱われます。
C#public class UserClass
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
var user1 = new UserClass { Id = 1, Name = "Taro" };
var user2 = new UserClass { Id = 1, Name = "Taro" };
Console.WriteLine(user1 == user2); // False
一方で、recordでは中身の値が同じなら等しいと判定されます。
C#public record UserRecord(int Id, string Name);
var user1 = new UserRecord(1, "Taro");
var user2 = new UserRecord(1, "Taro");
Console.WriteLine(user1 == user2); // True
これは、DTOや値オブジェクトのように「同じ値なら同じもの」と考えたい型で便利です。
Microsoft Learnでは、通常のclassは同じメモリ上のオブジェクトを参照している場合に等しい一方、recordは同じ型で同じ値を格納していれば等しいと説明されています。
3-2. ToStringが自動で見やすくなる
recordでは、ToString()の出力が自動で見やすくなります。
C#public record User(int Id, string Name);
var user = new User(1, "Taro");
Console.WriteLine(user);
出力例は次のとおりです。
User { Id = 1, Name = Taro }
通常のclassでは、ToString()をオーバーライドしない限り、型名だけが表示されることが多いです。
C#public class UserClass
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
var user = new UserClass { Id = 1, Name = "Taro" };
Console.WriteLine(user);
// UserClass のような型名だけが表示される
ログ出力やデバッグ時には、recordのToString()がとても便利です。
Microsoft Learnでも、レコード型にはパブリックなプロパティやフィールドの名前と値を表示するToString()がコンパイラーによって生成されると説明されています。
3-3. with式で一部だけ変更したコピーを作れる
recordでは、with式を使って一部の値だけを変えたコピーを作れます。
C#public record User(int Id, string Name);
var user1 = new User(1, "Taro");
var user2 = user1 with { Name = "Jiro" };
Console.WriteLine(user1); // User { Id = 1, Name = Taro }
Console.WriteLine(user2); // User { Id = 1, Name = Jiro }
ポイントは、元のuser1は変更されないことです。新しいuser2が作られます。
このような変更方法を、非破壊的変更と呼ぶことがあります。元のデータを壊さずに、新しいデータを作る考え方です。
検索条件や設定値など、一部だけ変更したパターンを作りたいときに便利です。
C#public record SearchCondition(string Keyword, int Page, int PageSize);
var condition1 = new SearchCondition("C#", 1, 20);
var condition2 = condition1 with { Page = 2 };
with式は、既存のインスタンスをコピーし、指定したプロパティやフィールドだけ変更した新しいインスタンスを作る構文です。ただし、参照型のプロパティについては参照がコピーされる浅いコピーである点に注意が必要です。
3-4. Deconstructで分解できる
プライマリコンストラクターを使ったrecordでは、Deconstructも自動生成されます。
C#public record User(int Id, string Name);
var user = new User(1, "Taro");
var (id, name) = user;
Console.WriteLine(id); // 1
Console.WriteLine(name); // Taro
これは、タプルのように値を分解して取り出したいときに便利です。
C#public record Point(int X, int Y);
var point = new Point(10, 20);
var (x, y) = point;
Console.WriteLine($"{x}, {y}");
パターンマッチングと組み合わせると、さらに便利に使えます。
C#public record Order(int Id, decimal TotalAmount);
var order = new Order(1, 12000m);
if (order is (_, > 10000m))
{
Console.WriteLine("高額注文です");
}
3-5. initアクセサによる初期化後の変更制限
recordでは、initアクセサがよく使われます。
C#public record User
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
initは、オブジェクトの初期化時には値を設定できるが、その後は変更できないアクセサです。
C#var user = new User
{
Id = 1,
Name = "Taro"
};
// user.Name = "Jiro"; // コンパイルエラー
setの場合は後から変更できます。
C#public record User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
recordのメリットを活かすなら、基本的にはinitを使い、後から自由に変更できる状態を避けるほうが扱いやすいです。
Microsoft Learnでは、initアクセサはオブジェクト初期化時に値を設定でき、初期化後は変更できないアクセサとして説明されています。
3-6. recordは完全な不変オブジェクトとは限らない
recordは不変オブジェクトのように扱われることがありますが、必ず完全に不変になるわけではありません。
たとえば、次のようにsetを使えば後から変更できます。
C#public record User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
また、initを使っていても、プロパティがList<T>や配列のような参照型の場合、中身は変更できてしまいます。
C#public record Team(string Name, List<string> Members);
var team = new Team("Dev", new List<string> { "Taro" });
team.Members.Add("Jiro");
Console.WriteLine(string.Join(", ", team.Members));
// Taro, Jiro
つまり、recordは「不変にしやすい型」ではありますが、「常に完全に不変な型」ではありません。
Microsoft Learnでも、init-onlyプロパティは浅い不変性であり、参照型プロパティが指す先のデータは変更できる場合があると説明されています。
4. C# recordとclassの違い
recordとclassの違いは、C# recordを理解するうえで最も重要です。
どちらも参照型として使えますが、設計思想が違います。
4-1. 最大の違いは「参照の同一性」か「値の等価性」か
classは、基本的に参照の同一性を重視します。
つまり、「同じオブジェクトを指しているか」が重要です。
C#public class UserClass
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
var a = new UserClass { Id = 1, Name = "Taro" };
var b = new UserClass { Id = 1, Name = "Taro" };
Console.WriteLine(a == b); // False
aとbは中身が同じですが、別々に作られたインスタンスなのでFalseになります。
一方でrecordは、値の等価性を重視します。
C#public record UserRecord(int Id, string Name);
var a = new UserRecord(1, "Taro");
var b = new UserRecord(1, "Taro");
Console.WriteLine(a == b); // True
中身の値が同じなのでTrueになります。
この違いが、classとrecordの使い分けの中心です。
4-2. classは振る舞い中心、recordはデータ中心
classは、データだけでなく振る舞いを持つオブジェクトに向いています。
C#public class Order
{
public int Id { get; private set; }
public decimal TotalAmount { get; private set; }
public bool IsConfirmed { get; private set; }
public void Confirm()
{
if (TotalAmount <= 0)
{
throw new InvalidOperationException("注文金額が不正です。");
}
IsConfirmed = true;
}
}
このOrderは単なるデータの入れ物ではありません。Confirm()という振る舞いを持ち、状態も変化します。
一方、recordは次のようなデータ中心の型に向いています。
C#public record OrderDto(int Id, decimal TotalAmount, bool IsConfirmed);
この型は、注文データを運ぶための型です。複雑な振る舞いを持たせるより、データを明確に表すことが目的です。
4-3. ==演算子の比較結果の違い
classとrecordでは、==の結果が違うことがあります。
C#public class PersonClass
{
public string Name { get; init; } = "";
}
public record PersonRecord(string Name);
var class1 = new PersonClass { Name = "Taro" };
var class2 = new PersonClass { Name = "Taro" };
var record1 = new PersonRecord("Taro");
var record2 = new PersonRecord("Taro");
Console.WriteLine(class1 == class2); // False
Console.WriteLine(record1 == record2); // True
classは参照が同じかを比較します。recordは値が同じかを比較します。
この違いを知らないままrecordを使うと、「別インスタンスなのにtrueになる」と驚くかもしれません。しかし、これはrecordの重要な特徴です。
4-4. 継承・仮想メンバー・sealed指定の違い
record classは継承できます。ただし、通常のclassとの継承関係には制限があります。
C#public record Person(string Name);
public record Employee(string Name, int EmployeeId) : Person(Name);
このように、recordは別のrecordを継承できます。
一方で、recordが通常のclassを継承したり、通常のclassがrecordを継承したりする設計はできません。
C#public class BaseClass
{
}
// public record MyRecord(string Name) : BaseClass; // 不可
また、継承をさせたくない場合はsealedを使えます。
C#public sealed record User(int Id, string Name);
recordの継承は便利な場面もありますが、値の等価性や型階層が絡むため、初心者のうちは多用しないほうが理解しやすいです。
Microsoft Learnでは、recordは別のrecordを継承できる一方、recordはclassを継承できず、classもrecordを継承できないと説明されています。
4-5. classを使うべきケース
次のような場合は、recordよりclassを使うほうが自然です。
状態が変化するオブジェクトを表したい場合。
C#public class ShoppingCart
{
private readonly List<CartItem> _items = new();
public void AddItem(CartItem item)
{
_items.Add(item);
}
}
複雑なビジネスロジックを持つ場合。
C#public class BankAccount
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("入金額が不正です。");
}
Balance += amount;
}
}
同じIDを持っていても、インスタンスの同一性を重視したい場合。
C#public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
Entity FrameworkなどのORMでエンティティとして扱う場合も、recordではなく通常のclassを選ぶことが多いです。
4-6. recordを使うべきケース
次のような場合は、recordが向いています。
DTOを表したい場合。
C#public record UserDto(int Id, string Name);
APIレスポンスを表したい場合。
C#public record WeatherResponse(string Area, double Temperature);
値オブジェクトを表したい場合。
C#public record EmailAddress(string Value);
設定値を表したい場合。
C#public record MailSettings(string Host, int Port, bool UseSsl);
テストデータを作りやすくしたい場合。
C#var user = new UserDto(1, "Taro");
var anotherUser = user with { Name = "Jiro" };
目安として、「この型はデータを表すだけか」「値が同じなら同じものとして扱いたいか」と考えると、recordを使うべきか判断しやすくなります。
5. C# recordとstructの違い
次に、recordとstructの違いを見ていきます。
ここで重要なのは、recordには参照型のrecord classと値型のrecord structがあることです。
5-1. structは値型、record classは参照型
通常のstructは値型です。
C#public struct Point
{
public int X { get; init; }
public int Y { get; init; }
}
一方、通常のrecordはrecord classの省略形なので参照型です。
C#public record PointRecord(int X, int Y);
参照型と値型では、代入やメソッド呼び出し時の挙動が異なります。
参照型は、変数にオブジェクトへの参照が入ります。値型は、変数に値そのものが入ります。
この違いは、パフォーマンスやメモリ効率だけでなく、バグの起き方にも影響します。
5-2. record structは値型のrecord
record structは、値型として使えるrecordです。
C#public record struct Point(int X, int Y);
通常のstructと同じく値型ですが、recordとしての便利機能も持ちます。
たとえば、ToString()は見やすく出力されます。
C#public record struct Point(int X, int Y);
var point = new Point(10, 20);
Console.WriteLine(point);
// Point { X = 10, Y = 20 }
値の等価性も使えます。
C#var p1 = new Point(10, 20);
var p2 = new Point(10, 20);
Console.WriteLine(p1 == p2); // True
C# 10ではrecord structが追加され、record classと同じような自動生成機能を値型でも使えるようになりました。
5-3. 代入時にコピーされるか、参照が共有されるか
参照型のrecord classでは、変数を代入すると参照がコピーされます。
C#public record User(string Name);
var user1 = new User("Taro");
var user2 = user1;
Console.WriteLine(ReferenceEquals(user1, user2)); // True
user1とuser2は同じインスタンスを指しています。
一方、値型のrecord structでは、代入時に値がコピーされます。
C#public record struct Point(int X, int Y);
var p1 = new Point(10, 20);
var p2 = p1;
p2.X = 30;
Console.WriteLine(p1); // Point { X = 10, Y = 20 }
Console.WriteLine(p2); // Point { X = 30, Y = 20 }
p2を変更しても、p1には影響しません。
ただし、record structの中に参照型プロパティがある場合、その参照先までは自動で深くコピーされない点に注意が必要です。
5-4. メモリ・パフォーマンス面の考え方
structやrecord structは値型なので、小さな値を大量に扱う場面で有利になることがあります。
たとえば、座標やサイズのような小さなデータです。
C#public readonly record struct Size(int Width, int Height);
public readonly record struct Point(int X, int Y);
ただし、値型は常に速いとは限りません。
大きなstructを何度もコピーすると、かえってコストが増える場合があります。また、ボックス化が発生するとパフォーマンスが悪化することもあります。
初心者のうちは、「パフォーマンスが良さそうだから」という理由だけでrecord structを選ぶのは避けたほうが安全です。
まずは設計上、値型として自然かどうかを考えるべきです。
5-5. structを使うべきケース
通常のstructは、次のような場面で使います。
小さく、単純な値を表す場合。
C#public struct Range
{
public int Start { get; init; }
public int End { get; init; }
}
C#や.NETの慣習として値型が自然な場合。
C#public struct Percentage
{
public double Value { get; init; }
}
ただし、通常のstructで等価性やToString()を丁寧に実装しようとすると、コード量が増えます。
そのため、データ中心の小さな値型であれば、record structも候補になります。
5-6. record structを使うべきケース
record structは、次のような場面で便利です。
小さな値型を簡潔に書きたい場合。
C#public readonly record struct Point(int X, int Y);
値型として扱いたいが、recordの便利機能も使いたい場合。
C#public readonly record struct Money(decimal Amount, string Currency);
大量に生成される小さなデータを扱う場合。
C#public readonly record struct Pixel(int X, int Y);
ただし、record structは値型なので、参照型のrecordとは使い勝手が違います。特に、代入時のコピー、既定値、可変性、ボックス化には注意しましょう。
6. record classとrecord structの選び方
record classとrecord structのどちらを選ぶべきかは、C# recordでよく迷うポイントです。
結論から言うと、初心者や一般的な業務アプリでは、まずrecord classを選ぶと理解しやすいです。
6-1. 基本はrecord classを選ぶと理解しやすい
通常のrecordはrecord classの省略形です。
C#public record UserDto(int Id, string Name);
この書き方は参照型です。
多くのDTO、APIレスポンス、設定値、検索条件では、record classで十分です。
C#public record LoginRequest(string Email, string Password);
public record LoginResponse(string AccessToken, DateTime ExpiresAt);
public record AppSettings(string ApiBaseUrl, int TimeoutSeconds);
参照型なので、既存のclassに近い感覚で扱えます。null許容や参照共有の考え方は必要ですが、業務アプリでは扱いやすい選択肢です。
6-2. 小さくて大量に扱う値ならrecord structを検討する
record structを検討するのは、小さな値を大量に扱う場合です。
C#public readonly record struct Point(int X, int Y);
public readonly record struct Size(int Width, int Height);
たとえば、ゲーム、画像処理、数値計算、座標計算などでは、値型のほうが自然なことがあります。
ただし、DTOやAPIレスポンスに対して何でもrecord structを使う必要はありません。
C#public record UserDto(int Id, string Name); // 多くの場合はこちらで十分
record structは、値型の性質を理解したうえで選びましょう。
6-3. 変更不可に近づけたいならreadonly record structを検討する
record structを使う場合、変更不可に近づけたいならreadonly record structを検討します。
C#public readonly record struct Point(int X, int Y);
readonlyを付けると、値型としての意図がより明確になります。
特に、座標、金額、範囲、サイズのように、一度作ったら値を変えないほうが自然なものに向いています。
C#public readonly record struct Money(decimal Amount, string Currency);
ただし、readonly record structであっても、参照型プロパティが指す先の中身まで完全に不変になるとは限りません。recordの不変性は浅い不変性になりやすい点を覚えておきましょう。
6-4. 参照型・値型の違いから選ぶ
record classとrecord structは、単に構文が違うだけではありません。参照型か値型かが違います。
判断の基準は次のとおりです。
データが大きい、または通常のオブジェクトとして扱いたいならrecord class。
C#public record UserProfile(
int Id,
string Name,
string Email,
string Address,
DateTime CreatedAt
);
小さく、値そのものとして扱いたいならrecord struct。
C#public readonly record struct Point(int X, int Y);
「同じ値なら同じもの」という意味を持ち、かつ値型として自然ならrecord structを検討します。
6-5. record class / record struct / class / structの比較表
| 種類 | 型の分類 | 等価性 | 主な用途 | 初心者向けの使いやすさ |
|---|---|---|---|---|
class | 参照型 | 基本は参照の同一性 | 振る舞いを持つオブジェクト、エンティティ | 高い |
record class / record | 参照型 | 値ベース | DTO、APIレスポンス、値オブジェクト | 高い |
struct | 値型 | 値ベースだが実装に注意 | 小さな値、低レベルな値型 | 中 |
record struct | 値型 | 値ベース | 小さなデータ中心の値型 | 中 |
readonly record struct | 値型 | 値ベース | 変更不可に近い小さな値 | 中 |
まずは次のように考えるとわかりやすいです。
アプリケーションの振る舞いを持つならclass。
C#public class OrderService
{
}
データを運ぶだけならrecord。
C#public record OrderDto(int Id, decimal TotalAmount);
小さな値そのものを表すならreadonly record struct。
C#public readonly record struct Quantity(int Value);
6-6. 迷ったときの判断フローチャート
迷ったときは、次の順番で考えると判断しやすいです。
その型はデータ中心ですか?
├─ いいえ → classを検討
└─ はい
↓
中身の値が同じなら同じものとして扱いたいですか?
├─ いいえ → classを検討
└─ はい
↓
小さくて値型として扱うのが自然ですか?
├─ いいえ → record classを選ぶ
└─ はい
↓
後から変更しない値ですか?
├─ はい → readonly record structを検討
└─ いいえ → record structを検討
実務では、DTOやAPIレスポンスならrecord class、座標や金額のような小さな値ならreadonly record struct、状態や振る舞いを持つものならclassという判断が多くなります。
7. C# recordの使いどころ
ここからは、C# recordの具体的な使いどころを見ていきます。
7-1. DTOやAPIレスポンスの受け皿
recordが特に使いやすいのがDTOです。
DTOとは、Data Transfer Objectの略で、データをやり取りするための型です。
C#public record UserDto(int Id, string Name, string Email);
APIのレスポンスを受け取る型にも向いています。
C#public record UserResponse(
int Id,
string Name,
string Email,
DateTime CreatedAt
);
リクエスト用の型にも使えます。
C#public record CreateUserRequest(
string Name,
string Email,
string Password
);
DTOは基本的にデータを運ぶだけなので、recordとの相性がよいです。
7-2. 値オブジェクト
値オブジェクトとは、IDではなく値そのもので識別されるオブジェクトです。
たとえば、メールアドレスを表す型です。
C#public record EmailAddress(string Value);
同じメールアドレス文字列を持っていれば、同じ値として扱えます。
C#var email1 = new EmailAddress("taro@example.com");
var email2 = new EmailAddress("taro@example.com");
Console.WriteLine(email1 == email2); // True
金額を表す値オブジェクトもrecordで表現できます。
C#public record Money(decimal Amount, string Currency);
より小さな値型として扱いたいなら、readonly record structも候補です。
C#public readonly record struct Money(decimal Amount, string Currency);
値オブジェクトは、値の等価性が重要なのでrecordと相性がよいです。
7-3. 設定値やオプションの表現
アプリケーション設定やオプション値にもrecordは向いています。
C#public record MailOptions(
string Host,
int Port,
bool UseSsl,
string FromAddress
);
設定値は、基本的に読み込んだ後に頻繁に変更しないことが多いため、initやrecordの考え方と合います。
C#public record AppOptions
{
public string ApiBaseUrl { get; init; } = "";
public int TimeoutSeconds { get; init; } = 30;
public bool EnableCache { get; init; } = true;
}
with式を使えば、一部だけ変更した設定を作ることもできます。
C#var defaultOptions = new AppOptions
{
ApiBaseUrl = "https://api.example.com",
TimeoutSeconds = 30,
EnableCache = true
};
var testOptions = defaultOptions with
{
ApiBaseUrl = "https://localhost:5001"
};
7-4. テストデータの作成
recordはテストコードでも便利です。
たとえば、テスト用のユーザーを作ります。
C#public record UserDto(int Id, string Name, string Email);
var baseUser = new UserDto(1, "Taro", "taro@example.com");
別パターンのユーザーを作りたいときは、with式で一部だけ変えられます。
C#var userWithoutEmail = baseUser with { Email = "" };
var anotherUser = baseUser with { Id = 2, Name = "Jiro" };
毎回すべてのプロパティを指定し直す必要がないため、テストデータの準備が楽になります。
特にプロパティ数が多いDTOでは、with式の効果が大きくなります。
C#var premiumUser = baseUser with
{
Name = "Premium Taro"
};
7-5. パターンマッチングと組み合わせる場面
recordはパターンマッチングとも相性がよいです。
C#public record Payment(decimal Amount, string Method);
var payment = new Payment(12000m, "CreditCard");
var message = payment switch
{
{ Amount: >= 10000m } => "高額決済です",
{ Method: "Cash" } => "現金決済です",
_ => "通常決済です"
};
Console.WriteLine(message);
プライマリコンストラクターを使ったrecordでは、分解パターンも使いやすくなります。
C#public record Point(int X, int Y);
var point = new Point(10, 20);
var area = point switch
{
(0, 0) => "原点",
(> 0, > 0) => "第1象限",
_ => "その他"
};
条件分岐をデータの形に基づいて書きたい場合、recordとパターンマッチングは強力な組み合わせになります。
7-6. Entity FrameworkなどORMで使うときの注意点
Entity FrameworkなどのORMでエンティティを表す場合、recordの使用には注意が必要です。
ORMのエンティティは、単なるデータの入れ物ではなく、同一性や変更追跡が重要になることが多いです。
C#public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
エンティティでは、「値が同じなら同じ」ではなく、「同じIDの同じ実体か」「同じインスタンスとして追跡されているか」が重要になる場合があります。
そのため、ORMのエンティティには通常のclassを使い、DTOや値オブジェクトにrecordを使う、という分け方がわかりやすいです。
Microsoft Learnでも、Entity Framework Coreは概念上1つのエンティティに対して1つのインスタンスを使うために参照等価性に依存しており、レコードやレコード構造体はエンティティ型として適切ではないと説明されています。
8. recordを使うときの注意点・ハマりどころ
recordは便利ですが、万能ではありません。ここでは、初心者がハマりやすいポイントを整理します。
8-1. 参照型プロパティを持つと中身は変更できてしまう
recordでinitを使っていても、参照型プロパティの中身は変更できることがあります。
C#public record Team(string Name, List<string> Members);
var team = new Team("Dev", new List<string> { "Taro" });
team.Members.Add("Jiro");
Console.WriteLine(string.Join(", ", team.Members));
// Taro, Jiro
Membersプロパティ自体を別のリストに差し替えることはできなくても、リストの中身は変更できます。
C#// team.Members = new List<string>(); // これは不可
team.Members.Add("Saburo"); // これは可能
完全に変更を防ぎたい場合は、IReadOnlyList<T>や不変コレクションの利用を検討します。
C#public record Team(string Name, IReadOnlyList<string> Members);
ただし、IReadOnlyList<T>にしても元のリストが外部で変更される可能性はあります。必要に応じてコピーを作る設計が必要です。
8-2. with式は基本的に浅いコピー
with式は便利ですが、基本的に浅いコピーです。
C#public record Team(string Name, List<string> Members);
var team1 = new Team("Dev", new List<string> { "Taro" });
var team2 = team1 with { Name = "QA" };
team2.Members.Add("Jiro");
Console.WriteLine(string.Join(", ", team1.Members));
// Taro, Jiro
team2は新しいrecordですが、Membersのリストはteam1と共有されています。
このように、参照型プロパティを持つrecordでwith式を使うと、思わぬ共有が起きることがあります。
必要なら、明示的に新しいリストを作ります。
C#var team2 = team1 with
{
Name = "QA",
Members = team1.Members.ToList()
};
with式は、新しいオブジェクトを作る便利な構文ですが、深いコピーを自動で行うものではありません。
8-3. 配列やListを持つrecordの等価性に注意
recordは値ベースの等価性を持ちますが、配列やList<T>を持つ場合は注意が必要です。
C#public record Numbers(int[] Values);
var a = new Numbers(new[] { 1, 2, 3 });
var b = new Numbers(new[] { 1, 2, 3 });
Console.WriteLine(a == b); // Falseになる場合がある
配列は参照型なので、配列の中身が同じでも、別々の配列であれば参照が異なります。
一方で、同じ配列インスタンスを共有していれば、等しいと判定されることがあります。
C#var values = new[] { 1, 2, 3 };
var a = new Numbers(values);
var b = new Numbers(values);
Console.WriteLine(a == b); // True
つまり、recordの等価性は便利ですが、コレクションの中身を自動で比較してくれるとは限りません。
コレクションの内容まで含めた等価性が必要な場合は、自分で比較ロジックを実装するか、設計を見直しましょう。
8-4. 可変プロパティを入れるとrecordのメリットが薄れる
recordでもsetを使えば、後から値を変更できます。
C#public record User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
ただし、可変プロパティを多く入れると、recordのメリットが薄れます。
recordは、値の等価性やwith式と相性がよい型です。後から自由に変更されると、ハッシュコードや等価性の扱いが難しくなることがあります。
たとえば、辞書のキーに使っているrecordの値を後から変更すると、意図しない挙動につながる可能性があります。
C#public record UserKey
{
public int Id { get; set; }
}
このような型は、できるだけinitやコンストラクターで初期化し、その後は変更しない設計にするほうが安全です。
8-5. 継承を多用する設計には向かない場合がある
record classは継承できますが、継承を多用する設計には注意が必要です。
C#public abstract record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
public record Cat(string Name, string Color) : Animal(Name);
このような使い方が便利な場面もあります。
しかし、recordは値の等価性を持つため、継承階層が複雑になると、等価性の理解も複雑になります。
また、データの種類を表したいだけなら、継承よりも別の設計のほうが読みやすい場合もあります。
たとえば、シンプルな区分なら列挙型やプロパティで十分なこともあります。
C#public record Animal(string Name, AnimalType Type);
public enum AnimalType
{
Dog,
Cat
}
recordの継承は便利ですが、「データ型だから何でも継承する」という使い方は避けましょう。
8-6. パフォーマンス目的だけでrecord structを選ばない
record structは値型なので、パフォーマンス面で有利に見えるかもしれません。
しかし、パフォーマンス目的だけでrecord structを選ぶのは危険です。
値型はコピーされます。サイズが大きいとコピーコストが増えます。
C#public record struct LargeData(
int A,
int B,
int C,
int D,
int E,
int F,
int G,
int H
);
また、インターフェイス経由で扱う場合などにボックス化が発生することもあります。
多くの業務アプリでは、DTOやAPIレスポンスをrecord classで書いても十分です。
record structは、値型として自然で、小さく、コピーされても問題がないデータに使いましょう。
9. recordの実践コード例
ここからは、実践的なコード例を見ながら、recordの使い方を確認します。
9-1. classで書いたDTOをrecordに置き換える例
まず、従来のclassでDTOを書いた例です。
C#public class UserDto
{
public int Id { get; }
public string Name { get; }
public string Email { get; }
public UserDto(int id, string name, string email)
{
Id = id;
Name = name;
Email = email;
}
}
このDTOをrecordに置き換えると、次のようになります。
C#public record UserDto(int Id, string Name, string Email);
使い方は同じようにできます。
C#var user = new UserDto(1, "Taro", "taro@example.com");
Console.WriteLine(user.Id);
Console.WriteLine(user.Name);
Console.WriteLine(user.Email);
さらに、ToString()や値の等価性も自動で使えます。
C#var user1 = new UserDto(1, "Taro", "taro@example.com");
var user2 = new UserDto(1, "Taro", "taro@example.com");
Console.WriteLine(user1 == user2); // True
Console.WriteLine(user1); // UserDto { Id = 1, Name = Taro, Email = taro@example.com }
DTOのようにデータを表すだけの型では、recordにすることでコードがかなりシンプルになります。
9-2. 値オブジェクトをrecordで表現する例
メールアドレスを値オブジェクトとして表現してみます。
C#public record EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("メールアドレスは必須です。", nameof(value));
}
if (!value.Contains("@"))
{
throw new ArgumentException("メールアドレスの形式が不正です。", nameof(value));
}
Value = value;
}
public override string ToString() => Value;
}
この型は、メールアドレスという値を表します。
C#var email1 = new EmailAddress("taro@example.com");
var email2 = new EmailAddress("taro@example.com");
Console.WriteLine(email1 == email2); // True
同じ値なら同じものとして扱えるため、値オブジェクトとrecordは相性がよいです。
より簡潔に書くなら、次のような形もあります。
C#public record EmailAddress(string Value)
{
public override string ToString() => Value;
}
ただし、バリデーションをしっかり入れたい場合は、明示的なコンストラクターを使うとよいです。
9-3. with式で一部だけ値を変更する例
検索条件を表すrecordを考えます。
C#public record SearchCondition(
string Keyword,
int Page,
int PageSize,
string Sort
);
最初の検索条件を作ります。
C#var condition = new SearchCondition(
Keyword: "c# record",
Page: 1,
PageSize: 20,
Sort: "newest"
);
次のページに移動する場合は、Pageだけ変えたコピーを作れます。
C#var nextPage = condition with
{
Page = 2
};
並び順だけ変えたい場合も簡単です。
C#var sortedByPopular = condition with
{
Sort = "popular"
};
元のconditionは変更されません。
C#Console.WriteLine(condition);
// SearchCondition { Keyword = c# record, Page = 1, PageSize = 20, Sort = newest }
Console.WriteLine(nextPage);
// SearchCondition { Keyword = c# record, Page = 2, PageSize = 20, Sort = newest }
このように、with式は「一部だけ違うデータ」を作る場面で非常に便利です。
9-4. record classとrecord structの比較コード
record classとrecord structの違いをコードで確認します。
C#public record UserRecordClass(int Id, string Name);
public record struct UserRecordStruct(int Id, string Name);
record classは参照型です。
C#var user1 = new UserRecordClass(1, "Taro");
var user2 = user1;
Console.WriteLine(ReferenceEquals(user1, user2)); // True
record structは値型です。
C#var point1 = new UserRecordStruct(1, "Taro");
var point2 = point1;
point2.Name = "Jiro";
Console.WriteLine(point1); // UserRecordStruct { Id = 1, Name = Taro }
Console.WriteLine(point2); // UserRecordStruct { Id = 1, Name = Jiro }
ただし、どちらも値ベースの等価性を持ちます。
C#var a = new UserRecordClass(1, "Taro");
var b = new UserRecordClass(1, "Taro");
Console.WriteLine(a == b); // True
var x = new UserRecordStruct(1, "Taro");
var y = new UserRecordStruct(1, "Taro");
Console.WriteLine(x == y); // True
違いは、等価性だけではありません。参照型か値型かによって、代入、コピー、null、メモリ上の扱いが変わります。
9-5. よくあるコンパイルエラーと修正例
recordでよくあるエラーの1つは、initプロパティを初期化後に変更しようとするケースです。
C#public record User
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
var user = new User
{
Id = 1,
Name = "Taro"
};
// user.Name = "Jiro"; // エラー
修正するには、with式で新しいインスタンスを作ります。
C#var updatedUser = user with
{
Name = "Jiro"
};
別の例として、record structを変更不可だと思い込むケースがあります。
C#public record struct Point(int X, int Y);
var point = new Point(10, 20);
point.X = 30; // 変更できる
変更されたくない場合は、readonly record structを使います。
C#public readonly record struct Point(int X, int Y);
また、プライマリコンストラクターの引数名と明示的なプロパティ名を合わせる場合は、初期化を忘れないようにします。
C#public record User(string Name)
{
public string Name { get; init; } = Name;
}
このように、recordは便利ですが、自動生成される内容を理解して使うことが大切です。
10. C# recordに関するよくある質問
最後に、C# recordに関するよくある質問をまとめます。
10-1. recordはいつから使える?
recordはC# 9で導入されました。
C# 9では参照型のレコードが追加され、C# 10ではrecord structが追加されました。C# 9は.NET 5とともにリリースされ、C# 10は.NET 6世代の機能として登場しています。
そのため、古いプロジェクトでは使えない場合があります。
使用できるかどうかは、プロジェクトのターゲットフレームワークやC#の言語バージョン設定に依存します。
10-2. recordとclassはどちらを使えばいい?
データ中心ならrecord、振る舞い中心ならclassを選ぶと考えるとわかりやすいです。
DTOやAPIレスポンスならrecordが向いています。
C#public record UserDto(int Id, string Name);
状態変更やビジネスロジックを持つならclassが向いています。
C#public class Order
{
public void Confirm()
{
}
}
また、ORMのエンティティのように参照の同一性が重要なものは、通常のclassを使うほうが自然です。
10-3. recordは不変なの?
recordは不変にしやすいですが、必ず不変とは限りません。
次のようにsetを使えば変更できます。
C#public record User
{
public string Name { get; set; } = "";
}
また、initを使っていても、参照型プロパティの中身は変更できる場合があります。
C#public record Team(string Name, List<string> Members);
recordを不変に近づけたい場合は、init、readonly record struct、読み取り専用コレクション、不変コレクションなどを組み合わせる必要があります。
10-4. record structは積極的に使うべき?
初心者が最初からrecord structを積極的に使う必要はありません。
多くのDTOやAPIレスポンスでは、通常のrecord、つまりrecord classで十分です。
C#public record UserDto(int Id, string Name);
record structは、値型として扱うことが自然な小さなデータに向いています。
C#public readonly record struct Point(int X, int Y);
「速そうだから」という理由だけで選ぶのではなく、値型としての意味があるかを考えましょう。
10-5. recordは業務アプリで使っても問題ない?
問題ありません。
特に次のような用途では、業務アプリでも使いやすいです。
DTO。
C#public record CustomerDto(int Id, string Name);
APIリクエスト・レスポンス。
C#public record CreateCustomerRequest(string Name, string Email);
検索条件。
C#public record CustomerSearchCondition(string Keyword, int Page);
設定値。
C#public record SystemOptions(string BaseUrl, int TimeoutSeconds);
ただし、エンティティ、状態を持つドメインオブジェクト、変更追跡が必要なオブジェクトでは、通常のclassのほうが適している場合があります。
10-6. recordを使わない方がいいケースは?
次のような場合は、recordを使わないほうがよいことがあります。
インスタンスの同一性が重要な場合。
C#public class UserEntity
{
public int Id { get; set; }
}
状態が頻繁に変わる場合。
C#public class GamePlayer
{
public int Hp { get; private set; }
public void Damage(int value)
{
Hp -= value;
}
}
複雑な振る舞いを持つ場合。
C#public class OrderService
{
public void PlaceOrder()
{
}
}
継承やライフサイクル管理が複雑な場合。
C#public class DbContextManagedEntity
{
}
recordは便利ですが、すべての型を置き換えるものではありません。データ中心で、値の等価性が自然な型に使うのが基本です。
まとめ
C#のrecordは、データを表す型を簡潔に書くための便利な機能です。
通常のrecordはrecord classの省略形で、参照型です。DTO、APIレスポンス、設定値、値オブジェクトなどに向いています。
classとの大きな違いは、等価性の考え方です。classは基本的に参照の同一性を重視しますが、recordは中身の値が同じなら等しいと判定されます。
structとの違いも重要です。structは値型であり、通常のrecord classは参照型です。値型のrecordを使いたい場合は、record structやreadonly record structを使います。
選び方の目安は次のとおりです。
| 使いたいもの | 選択肢 |
|---|---|
| 振る舞いや状態変化を持つオブジェクト | class |
| DTOやAPIレスポンス | record / record class |
| 小さく値型として自然なデータ | record struct |
| 変更不可に近い小さな値 | readonly record struct |
| 通常の値型を細かく制御したい | struct |
初心者のうちは、まず通常のrecordをDTOやAPIレスポンスで使ってみるのがおすすめです。
C#public record UserDto(int Id, string Name);
慣れてきたら、値オブジェクト、with式、record struct、readonly record structを使い分けると、より読みやすく安全なC#コードを書けるようになります。

