C#という言語は、開発者の生産性を向上させるために進化を続けています。

特に近年のアップデートでは、冗長なコードを削減し、意図をより明確にするための構文が数多く導入されてきました。

その中でも、C# 12からクラスや構造体でも利用可能になった「プライマリコンストラクタ」は、日常的なコーディングスタイルを劇的に変える可能性を秘めた機能です。

これまで、コンストラクタで受け取った引数をプライベートなフィールドに代入するという作業は、C#における「お決まりのパターン」でした。

しかし、プライマリコンストラクタを活用することで、このボイラープレート(定型的なコード)を大幅に削減できます。

本記事では、プライマリコンストラクタの基本概念から、具体的な活用シーン、そして利用時の注意点までをプロの視点で詳しく解説します。

プライマリコンストラクタとは何か

プライマリコンストラクタとは、クラスや構造体の宣言時に直接引数を定義できる機能です。

もともとC# 9.0においてrecord型で導入された機能でしたが、C# 12からは通常のclassstructでも利用できるようになりました。

従来のコンストラクタとの違い

従来のC#では、クラスにデータを渡す際、以下の手順が必要でした。

  1. 非公開フィールド(private readonlyなど)を定義する。
  2. コンストラクタを定義し、引数を受け取る。
  3. コンストラクタの本体で、引数の値をフィールドに代入する。

プライマリコンストラクタを使用すると、これらのステップをクラス宣言の1行に集約することができます。

基本的な構文

プライマリコンストラクタの構文は非常にシンプルです。

クラス名の直後にカッコを付け、その中に引数を記述します。

C#
using System;

// プライマリコンストラクタを使用したクラス定義
public class Person(string name, int age)
{
    public void PrintInfo()
    {
        // 引数をそのままメソッド内で使用可能
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}

class Program
{
    static void Main()
    {
        var person = new Person("Alice", 30);
        person.PrintInfo();
    }
}
実行結果
Name: Alice, Age: 30

この例では、nameageという引数がクラス全体のスコープで有効になります。

明示的にフィールドを定義しなくても、クラス内のメソッドから直接参照できる点が最大の特徴です。

プライマリコンストラクタの動作原理とスコープ

プライマリコンストラクタを正しく使いこなすためには、その内部的な動作を理解しておく必要があります。

引数のスコープとキャプチャ

プライマリコンストラクタの引数は、クラス内のすべてのインスタンスメンバー(メソッド、プロパティ、初期化子)から参照可能です。

コンパイラは、引数がメンバー内で使用されている場合、必要に応じて自動的に隠しフィールドを作成し、値を保持(キャプチャ)します。

もし引数が一度もメンバーから参照されず、フィールドの初期化にのみ使用される場合は、隠しフィールドは作成されません。

これにより、メモリ効率を最適化しつつ、簡潔な記述が可能となっています。

フィールドの初期化への活用

プライマリコンストラクタの引数は、フィールドの初期化子として直接利用できます。

これは、依存性の注入(DI)などを行う際に非常に便利です。

C#
public class OrderService(IDatabase database)
{
    // 引数を使ってフィールドを初期化
    private readonly IDatabase _database = database;

    public void ProcessOrder(int orderId)
    {
        _database.Save(orderId);
    }
}

このように記述すると、従来の「コンストラクタ引数からフィールドへの代入」という手間が省けます。

レコード型とクラスにおける挙動の違い

ここで注意が必要なのが、record型におけるプライマリコンストラクタとの違いです。

C#において、クラスとレコードではプライマリコンストラクタの解釈が異なります。

特徴レコード型 (record)クラス型 (class)
プロパティの自動生成生成される (public init-only)生成されない
引数の役割公開プロパティの定義と初期化クラス内スコープのパラメータ
外部からのアクセスobj.Param で可能外部からは直接アクセス不可

クラスのプライマリコンストラクタでは、自動的に公開プロパティは作成されません。

引数はあくまで「クラス内で使える変数」として扱われます。

もし外部からアクセスさせたい場合は、明示的にプロパティを定義し、引数で初期化する必要があります。

実践的なユースケース:依存性の注入(DI)

プライマリコンストラクタが最も威力を発揮するのは、依存性の注入(Dependency Injection)を多用する開発現場です。

現代的なASP.NET Coreなどの開発では、多くのサービスをコンストラクタ経由で受け取ります。

従来の記述法

C#
public class ProductController : ControllerBase
{
    private readonly ILogger<ProductController> _logger;
    private readonly IProductService _productService;

    public ProductController(ILogger<ProductController> logger, IProductService productService)
    {
        _logger = logger;
        _productService = productService;
    }

    [HttpGet]
    public IActionResult Get()
    {
        _logger.LogInformation("Products requested.");
        return Ok(_productService.GetAll());
    }
}

プライマリコンストラクタによる記述法

C#
public class ProductController(ILogger<ProductController> logger, IProductService productService) 
    : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        logger.LogInformation("Products requested.");
        return Ok(productService.GetAll());
    }
}

一目でわかる通り、コードの行数が大幅に削減され、クラスの本質的なロジックに集中できる構成になりました。

フィールド定義と代入処理が消えたことで、視覚的なノイズが取り除かれています。

高度な利用方法と構文のルール

プライマリコンストラクタを使用する場合でも、複数のコンストラクタを定義したり、基底クラスを継承したりすることが可能です。

別のコンストラクタの定義

クラスにプライマリコンストラクタがある場合、追加で定義するすべてのコンストラクタは、必ずプライマリコンストラクタを呼び出す(転送する)必要があります。

これは、プライマリコンストラクタがクラスの「主」となる初期化経路であるためです。

C#
public class User(string username, string role)
{
    // 別のコンストラクタからプライマリコンストラクタを呼び出す
    public User(string username) : this(username, "Guest")
    {
        // 追加の初期化ロジック
    }

    public void Show() => Console.WriteLine($"{username} ({role})");
}

継承におけるプライマリコンストラクタ

基底クラスがプライマリコンストラクタを持っている場合、派生クラスの宣言部でその引数を渡す必要があります。

C#
public class BaseService(string connectionString);

// 派生クラスで基底クラスのコンストラクタに値を渡す
public class SpecializedService(string connectionString, int timeout) 
    : BaseService(connectionString)
{
    public void Print() => Console.WriteLine($"Timeout: {timeout}");
}

注意点とベストプラクティス

プライマリコンストラクタは非常に便利ですが、盲目的に導入すると予期せぬバグやメンテナンス性の低下を招く恐れがあります。

以下の注意点を念頭に置いておきましょう。

1. 引数の可変性(ミュータビリティ)

プライマリコンストラクタの引数は、クラス内で「変数」のように振る舞います。

つまり、クラス内のメソッドでその値を書き換えることが可能です。

C#
public class Counter(int count)
{
    public void Increment() => count++; // 引数の値を直接変更可能
    public int GetCount() => count;
}

しかし、引数を直接書き換える設計は、コードの可読性を下げる原因となります。

値を変更する必要がある場合は、明示的なフィールドに代入し、そのフィールドを操作するのが一般的です。

2. 二重保存(Double Storage)の問題

引数をフィールドの初期化に使用し、かつメソッド内でもその引数を直接参照した場合、メモリ上に「フィールドの値」と「キャプチャされた引数の値」が二重に保持される可能性があります。

C#
public class WarningExample(int value)
{
    private readonly int _value = value;

    public void Print()
    {
        // _value と value の両方が存在することになる
        Console.WriteLine(_value);
        Console.WriteLine(value);
    }
}

このような記述は避け、基本的には「フィールドへの代入にのみ使う」か「引数を直接メンバーで使う」かのどちらかに統一すべきです。

3. バリデーションの記述場所

プライマリコンストラクタには「本体(波カッコ)」がありません。

そのため、引数のチェック(nullチェックなど)をどこに書くべきか迷うことがあります。

この場合、フィールドの初期化時に関数を呼び出すか、プロパティの初期化子を利用します。

C#
public class ValidatedUser(string name)
{
    // ThrowIfNull等を利用して初期化時にチェック
    private readonly string _name = name ?? throw new ArgumentNullException(nameof(name));
}

複雑なバリデーションが必要な場合は、あえて従来のコンストラクタを使用するか、ファクトリメソッドを検討するのが賢明です。

まとめ

C#のプライマリコンストラクタは、単なる「記述を短くするだけの機能」ではありません。

それは、クラスの定義をより宣言的にし、コードの意図を明確にするための強力なツールです。

特に以下のような場面で大きなメリットを享受できます。

  • 依存性の注入(DI)を利用するサービスの定義。
  • シンプルなデータ保持クラスや、フィールド初期化が中心のクラス。
  • 冗長なボイラープレートを排除し、ロジックの可読性を高めたい場合。

一方で、クラスとレコードでの挙動の違いや、キャプチャによる隠しフィールドの存在など、C#特有の言語仕様を正しく理解しておくことも重要です。

まずは小さなクラスやDIを利用しているコンポーネントから導入し、その簡潔さを体感してみてください。

C#は今後も進化を続けますが、プライマリコンストラクタはその進化の歴史の中でも、日常のコーディングを最も「楽」にしてくれる機能の一つと言えるでしょう。

適切に活用し、よりクリーンでメンテナンス性の高いソースコードを目指しましょう。