C#のrecord classとは?classとの違い・使い方・record structまで初心者向けに徹底解説

はじめに

C#でデータを扱うクラスを作っていると、「同じ値を持つオブジェクト同士を比較したい」「DTOや設定値のようなデータ中心の型をもっと短く書きたい」と感じる場面があります。

そのようなときに便利なのが、C#のrecord classです。

record classは、従来のclassと同じく参照型ですが、値ベースの等価比較、ToStringの自動生成、with式によるコピー、initアクセサを使った初期化後の変更制限など、データを表現するための便利な機能を標準で備えています。

一方で、record classは万能ではありません。通常のclassstructrecord structとの違いを理解せずに使うと、意図しない比較結果や設計上の問題につながることもあります。

この記事では、C#のrecord classについて、初心者にもわかりやすいように、classとの違い、基本的な書き方、使い方、メリット・注意点、さらにrecord structとの違いまで詳しく解説します。

1. C#のrecord classとは?初心者向けにわかりやすく解説

1-1. record classの基本概念

C#のrecord classとは、データを表現するために使いやすい参照型のことです。

通常のclassは、オブジェクトの振る舞いや状態の変更を表現するためによく使われます。一方、record classは「値そのもの」を表現する用途に向いています。

たとえば、次のようなユーザー情報を表す型を考えてみます。

C#
public record class User(string Name, int Age);

この1行だけで、NameAgeを持つUser型を定義できます。

C#
var user1 = new User("Taro", 30);
var user2 = new User("Taro", 30);

Console.WriteLine(user1 == user2); // True

通常のclassでは、別々に作成したオブジェクトは参照が異なるため、基本的には同じとは判定されません。しかし、record classでは中身の値が同じであれば、同じものとして比較できます。

これがrecord classの大きな特徴です。

1-2. record classが登場した背景

従来のC#でデータ中心のクラスを作る場合、次のようなコードを書く必要がありました。

C#
public class User
{
public string Name { get; }
public int Age { get; }

public User(string name, int age)
{
Name = name;
Age = age;
}

public override bool Equals(object? obj)
{
if (obj is not User other)
{
return false;
}

return Name == other.Name && Age == other.Age;
}

public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}

public override string ToString()
{
return $"User {{ Name = {Name}, Age = {Age} }}";
}
}

値の比較、ハッシュコード、文字列表現などを正しく実装しようとすると、かなりのコード量になります。

しかし、record classを使うと、同じような目的の型を次のように短く書けます。

C#
public record class User(string Name, int Age);

record classは、データを表す型を簡潔に、安全に、読みやすく書くために導入された機能です。

1-3. record classでできること

record classを使うと、主に次のようなことができます。

C#
public record class Product(string Name, decimal Price);

このように定義するだけで、以下のような機能が利用できます。

C#
var product1 = new Product("Book", 1200m);
var product2 = new Product("Book", 1200m);

Console.WriteLine(product1 == product2);
// True

Console.WriteLine(product1);
// Product { Name = Book, Price = 1200 }

var product3 = product1 with { Price = 1500m };

Console.WriteLine(product3);
// Product { Name = Book, Price = 1500 }

record classでは、次の機能が自動的に用意されます。

値ベースの等価比較、EqualsGetHashCodeの自動生成、読みやすいToStringwith式によるコピー、プライマリコンストラクター、分解用のDeconstructなどです。

これらは、データを持つだけの型を作るときに非常に便利です。

1-4. record classが向いているデータ構造

record classは、状態を頻繁に変更するオブジェクトよりも、「一度作ったら基本的に値を変えないデータ」に向いています。

具体的には、次のような用途に適しています。

C#
public record class UserDto(string Id, string Name, string Email);

public record class Address(string PostalCode, string City, string Street);

public record class ApiResult(bool Success, string Message);

DTO、APIレスポンス、設定値、検索条件、値オブジェクト、イベントデータなどは、record classと相性がよい代表例です。

一方で、IDで同一性を判断するエンティティや、内部状態を頻繁に変更するオブジェクトには、通常のclassの方が向いている場合があります。

2. record classとclassの違い

2-1. 値の比較と参照の比較の違い

record classclassの最も重要な違いは、等価比較の考え方です。

通常のclassでは、同じ値を持っていても、別々に作成されたインスタンスは別物として扱われます。

C#
public class UserClass
{
public string Name { get; init; }
public int Age { get; init; }

public UserClass(string name, int age)
{
Name = name;
Age = age;
}
}

var user1 = new UserClass("Taro", 30);
var user2 = new UserClass("Taro", 30);

Console.WriteLine(user1 == user2); // False

user1user2は、プロパティの値は同じですが、別々のオブジェクトとして作成されています。そのため、通常のclassでは参照が同じかどうかが比較されます。

一方、record classでは、値が同じかどうかで比較されます。

C#
public record class UserRecord(string Name, int Age);

var user1 = new UserRecord("Taro", 30);
var user2 = new UserRecord("Taro", 30);

Console.WriteLine(user1 == user2); // True

このように、record classは「中身が同じなら同じ」と判断するデータ構造に適しています。

2-2. Equals・GetHashCodeの自動生成

通常のclassで値ベースの比較を実現するには、EqualsGetHashCodeを自分で実装する必要があります。

C#
public class PointClass
{
public int X { get; }
public int Y { get; }

public PointClass(int x, int y)
{
X = x;
Y = y;
}

public override bool Equals(object? obj)
{
return obj is PointClass other &&
X == other.X &&
Y == other.Y;
}

public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
}

record classなら、これを簡単に書けます。

C#
public record class PointRecord(int X, int Y);

これだけで、XYの値を使ったEqualsGetHashCodeが自動的に生成されます。

C#
var p1 = new PointRecord(10, 20);
var p2 = new PointRecord(10, 20);

Console.WriteLine(p1.Equals(p2)); // True

特に、DictionaryHashSetで値の比較を使いたい場合、record classは便利です。

2-3. ToStringの出力内容の違い

通常のclassToStringを呼び出すと、基本的には型名が表示されるだけです。

C#
public class UserClass
{
public string Name { get; init; }
public int Age { get; init; }

public UserClass(string name, int age)
{
Name = name;
Age = age;
}
}

var user = new UserClass("Taro", 30);

Console.WriteLine(user);
// UserClass

一方、record classでは、プロパティ名と値を含む見やすい文字列が自動的に出力されます。

C#
public record class UserRecord(string Name, int Age);

var user = new UserRecord("Taro", 30);

Console.WriteLine(user);
// UserRecord { Name = Taro, Age = 30 }

デバッグ時にオブジェクトの中身を確認しやすいのは、record classの大きなメリットです。

2-4. 不変性を前提にした設計

record classは、不変性を前提にした設計と相性がよい型です。

たとえば、プライマリコンストラクターを使って定義したrecord classのプロパティは、基本的にinitアクセサを持ちます。

C#
public record class User(string Name, int Age);

var user = new User("Taro", 30);

// user.Name = "Jiro"; // コンパイルエラー

initは、オブジェクト初期化時には値を設定できますが、初期化後には変更できません。

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

var user = new User
{
Name = "Taro",
Age = 30
};

// user.Age = 31; // コンパイルエラー

これにより、意図しない値の変更を防ぎやすくなります。

2-5. with式によるコピーの違い

record classでは、with式を使って、一部の値だけを変更した新しいオブジェクトを作成できます。

C#
public record class User(string Name, int Age);

var user1 = new User("Taro", 30);

var user2 = user1 with { Age = 31 };

Console.WriteLine(user1);
// User { Name = Taro, Age = 30 }

Console.WriteLine(user2);
// User { Name = Taro, Age = 31 }

with式は、元のオブジェクトを変更するのではなく、新しいオブジェクトを作成します。

これを非破壊的変更といいます。

通常のclassでも手動でコピー用のメソッドを作れば似たことはできますが、record classでは標準機能として使えるため簡潔です。

2-6. record classとclassの使い分け

record classclassは、どちらが優れているというより、用途が異なります。

データそのものを表現したい場合は、record classが向いています。

C#
public record class CustomerDto(string Name, string Email);

一方、状態や振る舞いを持つオブジェクトには、通常の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;
}
}

BankAccountのように、残高が変化し、メソッドによって状態を操作するオブジェクトは、classで表現する方が自然です。

目安として、「値が同じなら同じもの」と考えるならrecord class、「同じ値でも別の存在」と考えるならclassを使うと判断しやすくなります。

3. record classの基本的な書き方

3-1. 最もシンプルなrecord classの定義

最もシンプルなrecord classは、次のように定義できます。

C#
public record class User;

これは、プロパティを持たないrecord classです。

ただし、実際には何らかのデータを持たせることが多いため、次のように書くケースが一般的です。

C#
public record class User(string Name, int Age);

なお、C#では次のようにclassを省略して書くこともできます。

C#
public record User(string Name, int Age);

この場合、recordは参照型のレコード、つまりrecord classとして扱われます。

明示的に参照型であることを示したい場合は、record classと書くとわかりやすくなります。

3-2. プロパティを持つrecord class

通常のクラスと同じように、プロパティを定義する形式でもrecord classを書けます。

C#
public record class User
{
public string Name { get; init; } = "";
public int Age { get; init; }
}

この形式では、オブジェクト初期化子を使って値を設定します。

C#
var user = new User
{
Name = "Taro",
Age = 30
};

プライマリコンストラクターを使う書き方に比べると少し長くなりますが、プロパティごとに初期値や属性、コメントなどを細かく設定しやすいメリットがあります。

3-3. プライマリコンストラクターを使う書き方

record classでは、プライマリコンストラクターを使う書き方がよく使われます。

C#
public record class User(string Name, int Age);

この定義により、NameAgeのプロパティ、コンストラクター、DeconstructEqualsGetHashCodeToStringなどが自動的に生成されます。

使い方は次のとおりです。

C#
var user = new User("Taro", 30);

Console.WriteLine(user.Name); // Taro
Console.WriteLine(user.Age); // 30

短いコードでデータ構造を定義できるため、DTOやAPIレスポンスモデルに適しています。

3-4. initアクセサとの関係

record classとよく一緒に使われるのがinitアクセサです。

initは、オブジェクトの初期化時だけプロパティに値を設定できる仕組みです。

C#
public record class User
{
public string Name { get; init; } = "";
public int Age { get; init; }
}

次のように、初期化時には値を設定できます。

C#
var user = new User
{
Name = "Taro",
Age = 30
};

しかし、作成後に値を変更することはできません。

C#
// user.Name = "Jiro"; // コンパイルエラー

record classは不変オブジェクトを作りやすい構文ですが、必ず完全に不変になるわけではありません。プロパティにsetを使えば、作成後に変更できるrecord classも作れます。

C#
public record class MutableUser
{
public string Name { get; set; } = "";
}

そのため、不変性を重視する場合は、initや読み取り専用コレクションを意識して設計することが重要です。

3-5. 通常のメソッドやプロパティを追加する方法

record classには、通常のclassと同じようにメソッドや追加プロパティを定義できます。

C#
public record class User(string FirstName, string LastName)
{
public string FullName => $"{FirstName} {LastName}";

public void Print()
{
Console.WriteLine(FullName);
}
}

使い方は通常のクラスと同じです。

C#
var user = new User("Taro", "Yamada");

Console.WriteLine(user.FullName); // Taro Yamada
user.Print(); // Taro Yamada

ただし、record classはあくまでデータ中心の型として使うのが基本です。複雑な状態変更やビジネスロジックが増えてきた場合は、通常のclassとして設計した方がよい場合もあります。

4. record classの使い方をコード例で解説

4-1. DTOとして使う例

DTOとは、Data Transfer Objectの略で、データを受け渡すためのオブジェクトです。

record classはDTOと非常に相性がよいです。

C#
public record class UserDto(
int Id,
string Name,
string Email
);

使用例は次のとおりです。

C#
var user = new UserDto(
Id: 1,
Name: "Taro",
Email: "taro@example.com"
);

Console.WriteLine(user.Name); // Taro
Console.WriteLine(user.Email); // taro@example.com

DTOは、多くの場合、値を保持して別の層や外部システムに渡すことが目的です。そのため、値ベースの比較や簡潔な定義ができるrecord classが適しています。

4-2. APIレスポンスのモデルとして使う例

APIレスポンスを表すモデルにも、record classはよく使われます。

C#
public record class UserResponse(
int Id,
string Name,
string Email,
bool IsActive
);

たとえば、APIから取得した結果を次のように扱えます。

C#
var response = new UserResponse(
Id: 1,
Name: "Taro",
Email: "taro@example.com",
IsActive: true
);

Console.WriteLine(response);
// UserResponse { Id = 1, Name = Taro, Email = taro@example.com, IsActive = True }

ToStringで中身が見やすく表示されるため、ログ出力やデバッグ時にも便利です。

4-3. 設定値オブジェクトとして使う例

設定値は、一度読み込んだら基本的に変更しないことが多いため、record classと相性がよいです。

C#
public record class AppSettings(
string ApplicationName,
int TimeoutSeconds,
bool EnableLogging
);

使用例は次のとおりです。

C#
var settings = new AppSettings(
ApplicationName: "SampleApp",
TimeoutSeconds: 30,
EnableLogging: true
);

Console.WriteLine(settings.TimeoutSeconds); // 30

設定値を不変にしておくと、アプリケーションの途中で予期せず値が変わるリスクを減らせます。

4-4. with式で一部の値だけ変更する例

with式を使うと、元のオブジェクトを変更せずに、一部の値だけを変えた新しいオブジェクトを作れます。

C#
public record class SearchCondition(
string Keyword,
int Page,
int PageSize
);

検索条件のページ番号だけを変えたい場合は、次のように書けます。

C#
var condition1 = new SearchCondition(
Keyword: "C# record class",
Page: 1,
PageSize: 20
);

var condition2 = condition1 with { Page = 2 };

Console.WriteLine(condition1);
// SearchCondition { Keyword = C# record class, Page = 1, PageSize = 20 }

Console.WriteLine(condition2);
// SearchCondition { Keyword = C# record class, Page = 2, PageSize = 20 }

condition1は変更されず、Pageだけが異なるcondition2が新しく作成されます。

4-5. Deconstructで値を分解する例

プライマリコンストラクターを使ったrecord classでは、Deconstructも自動生成されます。

C#
public record class User(string Name, int Age);

次のように、オブジェクトから値を分解して取り出せます。

C#
var user = new User("Taro", 30);

var (name, age) = user;

Console.WriteLine(name); // Taro
Console.WriteLine(age); // 30

複数の値をまとめて扱い、必要な場面で分解したいときに便利です。

5. record classの主な機能

5-1. 値ベースの等価比較

record classの代表的な機能が、値ベースの等価比較です。

C#
public record class Money(decimal Amount, string Currency);

var money1 = new Money(1000m, "JPY");
var money2 = new Money(1000m, "JPY");
var money3 = new Money(1000m, "USD");

Console.WriteLine(money1 == money2); // True
Console.WriteLine(money1 == money3); // False

money1money2は別々のインスタンスですが、AmountCurrencyの値が同じなので等しいと判定されます。

この特徴は、Value Objectのような「値で同一性を判断する型」に適しています。

5-2. 自動生成されるToString

record classでは、ToStringが自動生成されます。

C#
public record class Product(string Name, decimal Price);

var product = new Product("Keyboard", 5000m);

Console.WriteLine(product);
// Product { Name = Keyboard, Price = 5000 }

通常のclassではToStringをオーバーライドしない限り、中身を見やすく表示できません。

record classなら、デバッグやログ確認で値の内容を把握しやすくなります。

5-3. with式による非破壊的変更

with式は、record classの便利な機能の一つです。

C#
public record class OrderStatus(
int OrderId,
string Status
);

var order1 = new OrderStatus(100, "Pending");

var order2 = order1 with { Status = "Shipped" };

Console.WriteLine(order1.Status); // Pending
Console.WriteLine(order2.Status); // Shipped

元のオブジェクトを変更せず、新しいオブジェクトを作るため、不変性を保ちながら状態の変化を表現できます。

5-4. initによる初期化後の変更制限

record classでは、initアクセサを使うことで、作成後に値を変更できないプロパティを定義できます。

C#
public record class Customer
{
public string Name { get; init; } = "";
public string Email { get; init; } = "";
}
C#
var customer = new Customer
{
Name = "Taro",
Email = "taro@example.com"
};

// customer.Email = "new@example.com"; // コンパイルエラー

値を変更したい場合は、with式を使って新しいインスタンスを作ります。

C#
var updatedCustomer = customer with
{
Email = "new@example.com"
};

このように、initwithを組み合わせることで、変更に強い設計がしやすくなります。

5-5. 継承とrecord classの関係

record classは、通常のclassと同じく継承できます。

C#
public record class Person(string Name);

public record class Employee(string Name, int EmployeeId) : Person(Name);

使用例は次のとおりです。

C#
var employee = new Employee("Taro", 1001);

Console.WriteLine(employee);
// Employee { Name = Taro, EmployeeId = 1001 }

ただし、record classの継承では、等価比較に型の情報も関係します。

C#
Person person = new Person("Taro");
Person employee = new Employee("Taro", 1001);

Console.WriteLine(person == employee); // False

値が一部同じでも、型が異なれば等しいとは判定されません。

継承を多用すると、等価比較の挙動が複雑になることがあります。そのため、record classでは継承よりも、シンプルなデータ構造として使う方がわかりやすい場合が多いです。

6. record classを使うメリット

6-1. コード量を減らせる

record classを使う最大のメリットの一つは、コード量を大幅に減らせることです。

通常のclassで、プロパティ、コンストラクター、EqualsGetHashCodeToStringをすべて実装すると、多くのコードが必要になります。

しかし、record classなら次のように書けます。

C#
public record class User(string Name, int Age);

これだけで、データを扱うために必要な基本機能が自動的に用意されます。

コード量が減ると、読みやすくなり、保守もしやすくなります。

6-2. 値の比較が簡単になる

record classでは、値が同じかどうかを簡単に比較できます。

C#
public record class Coordinate(int X, int Y);

var c1 = new Coordinate(10, 20);
var c2 = new Coordinate(10, 20);

Console.WriteLine(c1 == c2); // True

通常のclassで同じことをするには、EqualsGetHashCodeを正しく実装する必要があります。

値オブジェクトやDTOの比較を簡単にしたい場合、record classは非常に便利です。

6-3. 不変オブジェクトを作りやすい

record classは、initアクセサやwith式と組み合わせることで、不変オブジェクトを作りやすくなります。

C#
public record class Address(
string PostalCode,
string City,
string Street
);

値を変更したい場合は、元のオブジェクトを直接書き換えるのではなく、新しいオブジェクトを作ります。

C#
var address1 = new Address("100-0001", "Tokyo", "Chiyoda");

var address2 = address1 with
{
Street = "Marunouchi"
};

このような書き方にすると、どこで値が変わったのかを追いやすくなります。

6-4. DTOやValue Objectを簡潔に書ける

record classは、DTOやValue Objectを簡潔に書くのに向いています。

C#
public record class EmailAddress(string Value);

たとえば、メールアドレスを単なるstringではなく、専用の型として表現できます。

C#
var email1 = new EmailAddress("taro@example.com");
var email2 = new EmailAddress("taro@example.com");

Console.WriteLine(email1 == email2); // True

値として意味を持つデータを専用の型にすることで、コードの意図が明確になります。

6-5. バグを減らしやすい設計にできる

record classを使うと、意図しない変更を防ぎやすくなります。

C#
public record class TaxRate(decimal Value);

このような値オブジェクトを使うことで、単なる数値ではなく「税率」という意味を持った値として扱えます。

また、initによって初期化後の変更を制限すれば、処理の途中で値が書き換わるリスクを減らせます。

値の比較も自動で正しく行われるため、比較ロジックの実装ミスも減らしやすくなります。

7. record classの注意点・デメリット

7-1. 完全な不変オブジェクトではない場合がある

record classは不変オブジェクトを作りやすい機能ですが、必ず完全に不変になるわけではありません。

たとえば、次のようにsetを使うと、作成後でも値を変更できます。

C#
public record class User
{
public string Name { get; set; } = "";
}
C#
var user = new User { Name = "Taro" };
user.Name = "Jiro";

Console.WriteLine(user.Name); // Jiro

また、initを使っていても、プロパティの中身が可変オブジェクトであれば注意が必要です。

record classを使うだけで自動的に完全な不変性が保証されるわけではない点を理解しておきましょう。

7-2. 参照型プロパティを持つ場合の注意点

record classが参照型プロパティを持つ場合、with式は基本的に浅いコピーになります。

C#
public record class Team(string Name, List<string> Members);

var team1 = new Team("Dev Team", new List<string> { "Taro", "Jiro" });

var team2 = team1 with { Name = "New Dev Team" };

team2.Members.Add("Saburo");

Console.WriteLine(string.Join(", ", team1.Members));
// Taro, Jiro, Saburo

team2Membersを変更したのに、team1Membersにも影響しています。

これは、Membersのリスト自体は同じ参照を共有しているためです。

不変性を重視する場合は、List<T>ではなく、読み取り専用のコレクションや不変コレクションの利用を検討する必要があります。

C#
public record class Team(
string Name,
IReadOnlyList<string> Members
);

ただし、IReadOnlyList<T>も中身の実体が可変であれば変更される可能性があります。完全な不変性が必要な場合は、コレクションの扱いまで慎重に設計しましょう。

7-3. Entityには向かないケースがある

record classは、値が同じなら同じものとして扱う型に向いています。

しかし、Entityは通常、値ではなくIDで同一性を判断します。

C#
public class UserEntity
{
public int Id { get; private set; }
public string Name { get; private set; }

public UserEntity(int id, string name)
{
Id = id;
Name = name;
}

public void ChangeName(string name)
{
Name = name;
}
}

たとえば、データベース上のユーザーは、名前やメールアドレスが変わっても同じユーザーです。このような場合は、値全体で比較するrecord classよりも、通常のclassの方が自然です。

Entityをrecord classにすると、値の比較が意図とずれる可能性があります。

7-4. 継承を使う場合の注意点

record classは継承できますが、等価比較の挙動が複雑になりやすいです。

C#
public record class Animal(string Name);

public record class Dog(string Name, string Breed) : Animal(Name);

次のように、基底型として扱う場合も型の違いが比較に影響します。

C#
Animal animal = new Animal("Pochi");
Animal dog = new Dog("Pochi", "Shiba");

Console.WriteLine(animal == dog); // False

継承階層が深くなると、「どの値を同一性に含めるのか」がわかりにくくなることがあります。

データ中心の型では、継承よりも合成を使った方がシンプルな設計になる場合が多いです。

7-5. パフォーマンス面で意識すべき点

record classは参照型なので、インスタンスはヒープに確保されます。

また、with式を使うと新しいインスタンスが作成されます。

C#
var user2 = user1 with { Age = 31 };

これは便利ですが、大量のオブジェクトを頻繁にコピーする処理では、メモリ使用量やGCの負荷を意識する必要があります。

また、値ベースの比較では、プロパティの数や中身によって比較コストが増える場合があります。

通常の業務アプリケーションでは大きな問題にならないことが多いですが、高頻度で大量のデータを扱う処理では、record structや通常のstruct、あるいは通常のclassも含めて検討するとよいでしょう。

8. record classとrecord structの違い

8-1. record classは参照型、record structは値型

record classrecord structの大きな違いは、参照型か値型かです。

C#
public record class PointClass(int X, int Y);

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

record classは参照型です。変数にはオブジェクトへの参照が入ります。

一方、record structは値型です。変数には値そのものが入ります。

C#
var p1 = new PointStruct(10, 20);
var p2 = p1;

p2.X = 99;

Console.WriteLine(p1.X); // 10
Console.WriteLine(p2.X); // 99

値型であるrecord structでは、代入時に値がコピーされます。

8-2. メモリ管理とコピー動作の違い

record classは参照型なので、変数を代入すると参照がコピーされます。

C#
public record class UserClass
{
public string Name { get; set; } = "";
}

var user1 = new UserClass { Name = "Taro" };
var user2 = user1;

user2.Name = "Jiro";

Console.WriteLine(user1.Name); // Jiro

user1user2は同じオブジェクトを参照しています。

一方、record structは値型なので、代入時に値がコピーされます。

C#
public record struct UserStruct(string Name);

var user1 = new UserStruct("Taro");
var user2 = user1;

user2.Name = "Jiro";

Console.WriteLine(user1.Name); // Taro
Console.WriteLine(user2.Name); // Jiro

ただし、record structでも参照型のプロパティを持つ場合は、その参照がコピーされる点に注意が必要です。

8-3. readonly record structとは

readonly record structは、変更できない値型のレコードを作るための構文です。

C#
public readonly record struct Point(int X, int Y);

readonly record structにすると、プロパティが読み取り専用になり、値型としての不変性を保ちやすくなります。

C#
var point = new Point(10, 20);

// point.X = 30; // コンパイルエラー

小さくて不変な値を表す場合に便利です。

たとえば、座標、サイズ、範囲、金額などの値を表すときに使いやすいです。

C#
public readonly record struct Size(int Width, int Height);

8-4. record structを使うべきケース

record structは、次のようなケースに向いています。

小さな値を表す、値型として扱いたい、ヒープ割り当てを避けたい、コピーされても問題ない、不変にしたい場合です。

たとえば、座標を表す型はrecord structに向いています。

C#
public readonly record struct Coordinate(int X, int Y);

金額のような値も候補になります。

C#
public readonly record struct Money(decimal Amount, string Currency);

ただし、サイズが大きい構造体を頻繁にコピーすると、かえってパフォーマンスが悪くなる場合があります。

record structは、小さく、値として自然に扱えるデータに使うのが基本です。

8-5. record classとrecord structの使い分け

record classは参照型として扱いたいデータに向いています。

C#
public record class UserDto(string Name, string Email);

DTOやAPIレスポンスなど、ある程度大きなデータや、nullを許容したいデータにはrecord classが使いやすいです。

一方、record structは小さな値を表すときに向いています。

C#
public readonly record struct Point(int X, int Y);

判断基準としては、参照型として扱いたいならrecord class、小さな値型として扱いたいならrecord structを選ぶとよいでしょう。

9. record class・record struct・class・structの比較

9-1. それぞれの特徴一覧

C#には、classrecord classstructrecord structがあります。

それぞれの特徴を整理すると、次のようになります。

種類主な用途等価比較不変性
class参照型状態や振る舞いを持つオブジェクト基本は参照比較自由に設計
record class参照型データ中心のオブジェクト値ベース不変にしやすい
struct値型小さな値フィールド値ベースだが明示実装が望ましい自由に設計
record struct値型小さなデータ中心の値値ベース不変にしやすい

初心者がまず覚えるべきポイントは、classrecord classは参照型、structrecord structは値型という点です。

9-2. 等価比較の違い

通常のclassでは、基本的に参照が同じかどうかで比較されます。

C#
public class UserClass
{
public string Name { get; init; } = "";
}

var a = new UserClass { Name = "Taro" };
var b = new UserClass { Name = "Taro" };

Console.WriteLine(a == b); // False

record classでは、値が同じかどうかで比較されます。

C#
public record class UserRecord(string Name);

var a = new UserRecord("Taro");
var b = new UserRecord("Taro");

Console.WriteLine(a == b); // True

record structも値ベースで比較できます。

C#
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

等価比較を簡潔に扱いたい場合は、record classrecord structが便利です。

9-3. 可変性・不変性の違い

classstructは、設計次第で可変にも不変にもできます。

C#
public class MutableUser
{
public string Name { get; set; } = "";
}

record classは不変にしやすい構文ですが、setを使えば可変にもできます。

C#
public record class MutableRecord
{
public string Name { get; set; } = "";
}

プライマリコンストラクターを使ったrecord classは、初期化後にプロパティを変更できない形になりやすいです。

C#
public record class User(string Name);

record structも、通常は可変ですが、readonly record structを使うことで不変にしやすくなります。

C#
public readonly record struct Point(int X, int Y);

不変性を重視するなら、record classではinitrecord structではreadonlyを意識するとよいでしょう。

9-4. 用途別の選び方

用途別に選ぶなら、次のように考えるとわかりやすいです。

データベースのEntityや状態を持つオブジェクトには、通常のclassが向いています。

C#
public class Order
{
public int Id { get; private set; }
public string Status { get; private set; } = "Pending";

public void Ship()
{
Status = "Shipped";
}
}

DTOやAPIレスポンスには、record classが向いています。

C#
public record class OrderDto(int Id, string Status);

座標やサイズなどの小さな値には、readonly record structが向いています。

C#
public readonly record struct Size(int Width, int Height);

低レベルな最適化や既存APIとの互換性が重要な場合は、通常のstructを使うこともあります。

9-5. 初心者が迷ったときの判断基準

初心者が迷ったときは、まず次の基準で考えるとよいです。

「状態が変化するもの」ならclassを使います。

C#
public class ShoppingCart
{
private readonly List<string> _items = new();

public void AddItem(string item)
{
_items.Add(item);
}
}

「値が同じなら同じと考えたいデータ」ならrecord classを使います。

C#
public record class ProductDto(int Id, string Name);

「小さくて値型として扱いたいデータ」ならreadonly record structを検討します。

C#
public readonly record struct Point(int X, int Y);

最初から無理にすべてを使い分けようとせず、まずはclassrecord classの違いを理解することが大切です。

10. record classを使うべきケース・使わないほうがよいケース

10-1. 使うべきケース

record classを使うべきケースは、値を中心に扱うデータ構造です。

たとえば、DTOは代表的な例です。

C#
public record class UserDto(
int Id,
string Name,
string Email
);

APIのリクエストやレスポンスにも向いています。

C#
public record class CreateUserRequest(
string Name,
string Email
);

設定値にも適しています。

C#
public record class DatabaseSettings(
string ConnectionString,
int CommandTimeout
);

また、値オブジェクトにも使いやすいです。

C#
public record class UserName(string Value);

このように、「データを表すこと」が主な目的で、「値が同じなら同じ」と考えられるものはrecord classの候補になります。

10-2. 使わないほうがよいケース

record classを使わないほうがよいケースは、値全体ではなく、個別の同一性を持つオブジェクトです。

たとえば、ユーザーEntityは、名前やメールアドレスが変わっても同じユーザーです。

C#
public class UserEntity
{
public int Id { get; private set; }
public string Name { get; private set; }

public UserEntity(int id, string name)
{
Id = id;
Name = name;
}

public void ChangeName(string name)
{
Name = name;
}
}

このような型は、record classではなく通常のclassで表現する方が自然です。

また、内部状態を頻繁に変更するオブジェクト、複雑な振る舞いを持つドメインオブジェクト、継承階層が複雑な型にも、record classは向かない場合があります。

10-3. DTO・Value Object・Entityでの使い分け

DTO、Value Object、Entityの使い分けを理解すると、record classを使うべきか判断しやすくなります。

DTOは、データの受け渡しを目的とするため、record classが向いています。

C#
public record class UserDto(int Id, string Name);

Value Objectは、値そのものに意味があり、値が同じなら同じものとして扱うため、record classが向いています。

C#
public record class EmailAddress(string Value);

Entityは、値ではなくIDで同一性を判断することが多いため、通常のclassが向いています。

C#
public class User
{
public int Id { get; private set; }
public string Email { get; private set; }

public User(int id, string email)
{
Id = id;
Email = email;
}
}

すべてをrecord classにするのではなく、役割に応じて使い分けることが重要です。

10-4. 実務での判断ポイント

実務でrecord classを使うか迷ったら、次のように考えると判断しやすくなります。

まず、その型がデータ中心かどうかを確認します。データを運ぶだけ、または値を表すだけなら、record classが向いています。

次に、値が同じなら同じものとして扱ってよいかを考えます。よい場合はrecord class、そうでない場合は通常のclassを検討します。

さらに、作成後に値を変更する必要があるかも重要です。変更しない、または変更時には新しいオブジェクトを作る設計なら、record classは適しています。

反対に、内部状態をメソッドで変更しながら扱うオブジェクトなら、通常のclassの方が自然です。

11. record classでよくあるエラーと疑問

11-1. recordとrecord classの違いは?

C#では、次のようにrecordと書くことができます。

C#
public record User(string Name, int Age);

これは、参照型のレコードを意味します。つまり、実質的にはrecord classです。

次のように明示的に書くこともできます。

C#
public record class User(string Name, int Age);

record classと書くと、参照型であることが明確になります。

一方、値型のレコードを作りたい場合は、record structを使います。

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

初心者のうちは、recordrecord classの省略形、record structは値型のレコード、と覚えるとわかりやすいです。

11-2. record classのプロパティは変更できる?

record classのプロパティが変更できるかどうかは、定義の仕方によります。

プライマリコンストラクターで定義した場合、基本的には初期化後に変更できません。

C#
public record class User(string Name, int Age);

var user = new User("Taro", 30);

// user.Age = 31; // コンパイルエラー

ただし、setを使えば変更可能なプロパティも作れます。

C#
public record class User
{
public string Name { get; set; } = "";
}
C#
var user = new User { Name = "Taro" };
user.Name = "Jiro";

つまり、record classだから必ず変更不可というわけではありません。

不変にしたい場合は、initや読み取り専用設計を意識する必要があります。

11-3. classからrecord classへ置き換えてもよい?

既存のclassrecord classへ置き換える場合は、慎重に判断する必要があります。

単なるDTOであれば、置き換えても問題ないケースが多いです。

C#
public class UserDto
{
public int Id { get; init; }
public string Name { get; init; } = "";
}

これは次のように書き換えられます。

C#
public record class UserDto(int Id, string Name);

しかし、状態を持つクラスや、IDで同一性を判断するEntityをrecord classに置き換えると、等価比較の意味が変わってしまう可能性があります。

特に、==EqualsGetHashCodeの挙動が変わる点に注意しましょう。

置き換える前に、そのクラスが「値として比較されてよい型か」を確認することが重要です。

11-4. record classはいつから使える?

recordはC# 9.0で導入されました。

その後、C# 10.0でrecord structが追加され、record classという書き方で参照型のレコードであることを明示できるようになりました。

そのため、record classを使うには、対応するC#のバージョンと.NET環境が必要です。

たとえば、古い.NET Framework環境や古いC#バージョンでは、そのまま使えない場合があります。

実務で使う場合は、プロジェクトのLangVersionやターゲットフレームワークを確認しましょう。

XML
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>

ただし、むやみにlatestを指定するより、チームやプロジェクトで利用するC#バージョンを明確に決める方が安全です。

11-5. record classは初心者でも使うべき?

初心者でも、record classはぜひ理解しておきたい機能です。

特に、DTOやAPIレスポンスのような単純なデータ型では、record classを使うとコードが短くなり、読みやすくなります。

C#
public record class UserResponse(
int Id,
string Name,
string Email
);

ただし、最初から何でもrecord classにする必要はありません。

まずは通常のclassを理解し、そのうえで「値として比較したいデータ」「変更しないデータ」「DTOのようなデータ受け渡し用の型」にrecord classを使うとよいでしょう。

record classは便利ですが、通常のclassの代わりではなく、データ中心の型を簡潔に書くための選択肢です。

まとめ

C#のrecord classは、データを表現するために便利な参照型です。

通常のclassとの大きな違いは、値ベースの等価比較が自動で行われる点です。さらに、EqualsGetHashCodeToStringwith式、Deconstructなど、データ中心の型に必要な機能が標準で用意されています。

C#
public record class User(string Name, int Age);

このように短く書くだけで、値を扱いやすい型を作成できます。

record classは、DTO、APIレスポンス、設定値、Value Objectのような用途に向いています。一方で、IDで同一性を判断するEntityや、状態を頻繁に変更するオブジェクトには、通常のclassの方が適している場合があります。

また、record classは参照型であり、record structは値型です。小さな値を表す場合はreadonly record structも選択肢になります。

初心者が迷ったときは、次のように考えるとよいでしょう。

データ中心で、値が同じなら同じものとして扱いたいならrecord class。状態や振る舞いを持ち、同じ値でも別の存在として扱いたいならclass。小さな値型として扱いたいならreadonly record struct

record classを正しく使えるようになると、C#のコードをより簡潔に、読みやすく、安全に書けるようになります。