Skip to content

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を検討します:

  1. 単一の値を論理的に表現(int、doubleなどの基本型に類似)
  2. インスタンスサイズが16バイト未満
  3. 不変である
  4. 頻繁にボックス化されることがない

WARNING

大きなstructはパフォーマンス上の問題を引き起こす可能性があります。16バイトを超える場合はclassまたはrecordを検討してください。

recordを選択する場合

以下の条件に当てはまる場合、recordが適しています:

  • 複雑な値をカプセル化する
  • 不変性が重要である
  • 単方向(一方向)のデータフローで使用する
  • 値ベースの等価性が必要である

classを選択する場合

上記の条件に当てはまらない場合、または以下が必要な場合はclassを使用します:

  • 継承やポリモーフィズム
  • ミュータブルな状態管理
  • 複雑なビジネスロジックのカプセル化

実践的な使用例

DTOとリクエストバインディング

質問の例であるSearchParametersは、recordを使用する理想的なケースです:

csharp
public record SearchParameters(
    string Query, 
    int Page, 
    int PageSize, 
    string SortBy);
csharp
public class HomeController : Controller
{ 
    public async Task<IActionResult> Search(SearchParameters searchParams)
    {
        await _service.SearchAsync(searchParams);
    }
}
csharp
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式を使って非破壊的変更が可能です:

csharp
var initialParams = new SearchParameters("query", 1, 10, "name");
var nextPageParams = initialParams with { Page = 2 };

recordの詳細な動作

コピーのセマンティクス

recordのコピー動作には注意が必要です:

csharp
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は、完全な不変性を保証する構造体です:

csharp
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の機能を持つ構造体です:

csharp
public record struct Point(double X, double Y);

まとめ

特徴classstructrecord
参照型値型参照型
等価性参照ベース値ベース値ベース
不変性オプションオプションデフォルト
継承可能不可可能
使用例ビジネスロジック小さな値型データ転送・不変オブジェクト

結論として:

  • DTOやリクエストパラメータにはrecordを使用
  • 16バイト以下の小さな不変値にはstructまたはreadonly structを使用
  • 複雑な振る舞いや状態管理が必要な場合はclassを使用

recordはC#の型システムに強力な追加機能をもたらし、不変データモデルの作成を簡素化します。適切な場面で活用することで、より安全で理解しやすいコードを書くことができます。