record vs class vs struct - C#での選択指針
C# 9で導入されたrecord型は、開発者に新たなデータ型の選択肢をもたらしました。class、struct、recordの使い分けについて、実践的な観点から解説します。
問題の背景
多くの開発者が以下の疑問を持っています:
- DTO(Data Transfer Object)にはrecordを使用すべきか?
- APIコントローラーのリクエストバインディングにrecordは適しているか?
- 具体的には、
SearchParameters
のようなパラメータクラスをrecordにするべきか?
INFO
DTO(Data Transfer Object)は、異なるレイヤー間でデータを転送するためのオブジェクトです。通常、ビジネスロジックを持たず、データの保持のみを目的とします。
各データ型の基本特性
class(クラス)
- 参照型:ヒープにメモリ確保
- 継承とポリモーフィズムをサポート
- 責任と動作の定義が主目的
- ミュータブル(変更可能)が基本
struct(構造体)
- 値型:スタックにメモリ確保
- 継承不可(インターフェース実装は可能)
- 16バイト以下の小さなデータ構造向け
- コピーが効率的
- 値のセマンティクス
record(レコード)
- 参照型だが値ベースの等価性
- 不変性を前提として設計
- 簡潔な構文と非破壊的変更(
with
式) - データ中心の型向け
選択フローチャート
詳細な選択基準
structを選択する場合
以下のすべての条件を満たす場合にstructを検討します:
- 単一の値を論理的に表現(int、doubleなどの基本型に類似)
- インスタンスサイズが16バイト未満
- 不変である
- 頻繁にボックス化されることがない
WARNING
大きなstructはパフォーマンス上の問題を引き起こす可能性があります。16バイトを超える場合はclassまたはrecordを検討してください。
recordを選択する場合
以下の条件に当てはまる場合、recordが適しています:
- 複雑な値をカプセル化する
- 不変性が重要である
- 単方向(一方向)のデータフローで使用する
- 値ベースの等価性が必要である
classを選択する場合
上記の条件に当てはまらない場合、または以下が必要な場合はclassを使用します:
- 継承やポリモーフィズム
- ミュータブルな状態管理
- 複雑なビジネスロジックのカプセル化
実践的な使用例
DTOとリクエストバインディング
質問の例であるSearchParameters
は、recordを使用する理想的なケースです:
public record SearchParameters(
string Query,
int Page,
int PageSize,
string SortBy);
public class HomeController : Controller
{
public async Task<IActionResult> Search(SearchParameters searchParams)
{
await _service.SearchAsync(searchParams);
}
}
public class SearchService
{
public async Task<List<Result>> SearchAsync(SearchParameters parameters)
{
// パラメータを使用して検索実行
var results = await _repository.Search(
parameters.Query,
parameters.Page,
parameters.PageSize,
parameters.SortBy);
return results;
}
}
recordの不変性と変更操作
recordはデフォルトで不変ですが、with
式を使って非破壊的変更が可能です:
var initialParams = new SearchParameters("query", 1, 10, "name");
var nextPageParams = initialParams with { Page = 2 };
recordの詳細な動作
コピーのセマンティクス
recordのコピー動作には注意が必要です:
var original = new MyRecord(new List<string>());
var shallowCopy = original; // 参照のコピー
var withCopy = original with { }; // 新しい参照、ただし参照型プロパティは共有
original.List.Add("item");
// shallowCopy.Listには"item"が含まれる
// withCopy.Listにも"item"が含まれる(同じ参照を共有)
パフォーマンス考慮事項
recordは値の比較を行うため、大規模なデータ構造ではパフォーマンスに影響する可能性があります。ただし、ほとんどの実践的なユースケースでは、このオーバーヘッドは無視できます。
TIP
データベースやファイルシステム操作を行うアプリケーションでは、通常、これらの操作の方がrecordのコピーコストよりもはるかに高くなります。
追加の型オプション
readonly struct
C# 7.2で導入されたreadonly struct
は、完全な不変性を保証する構造体です:
public readonly struct Point
{
public Point(double x, double y)
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
}
record struct
C# 10で導入されたrecord struct
は、recordの機能を持つ構造体です:
public record struct Point(double X, double Y);
まとめ
特徴 | class | struct | record |
---|---|---|---|
型 | 参照型 | 値型 | 参照型 |
等価性 | 参照ベース | 値ベース | 値ベース |
不変性 | オプション | オプション | デフォルト |
継承 | 可能 | 不可 | 可能 |
使用例 | ビジネスロジック | 小さな値型 | データ転送・不変オブジェクト |
結論として:
- DTOやリクエストパラメータには
record
を使用 - 16バイト以下の小さな不変値には
struct
またはreadonly struct
を使用 - 複雑な振る舞いや状態管理が必要な場合は
class
を使用
recordはC#の型システムに強力な追加機能をもたらし、不変データモデルの作成を簡素化します。適切な場面で活用することで、より安全で理解しやすいコードを書くことができます。