C# プライマリコンストラクタとは?使い方・従来コンストラクタとの違い・注意点を初心者向けに解説

はじめに

C# プライマリコンストラクタは、C# 12で導入された新しいコンストラクタ記法です。従来のようにクラス内にコンストラクタを別途書くのではなく、classstructの型名の横に引数を直接書けるため、初期化コードを短く、読みやすくできます。Microsoftの公式ドキュメントでも、C# 12ではclassstructにプライマリコンストラクタを宣言できるようになったと説明されています。

ただし、C# プライマリコンストラクタは「便利だから常に使えばよい」という機能ではありません。特に初心者がつまずきやすいのは、recordのプライマリコンストラクタとは違い、classstructでは引数が自動でプロパティにならない点です。つまり、class Person(string name)と書いても、外部からperson.Nameのように参照できるプロパティが自動生成されるわけではありません。

この記事では、C# プライマリコンストラクタとは何か、従来コンストラクタとの違い、基本的な使い方、注意点、よくあるエラー、recordとの違いまで、初心者にもわかりやすく解説します。

1. C# プライマリコンストラクタとは?

1-1. C# 12で使えるようになった新しいコンストラクタ記法

C# プライマリコンストラクタとは、クラスや構造体の宣言部分に直接コンストラクタの引数を書ける機能です。

たとえば、従来のC#では次のようにコンストラクタを書きます。

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

public User(string name)
{
Name = name;
}
}

C# 12のプライマリコンストラクタを使うと、次のように書けます。

C#
public class User(string name)
{
public string Name { get; } = name;
}

型名Userの横に(string name)と書いている部分が、プライマリコンストラクタです。

C# 12以前でもrecordでは似たような書き方ができましたが、C# 12からは通常のclassstructでも使えるようになりました。

1-2. 型名の横に引数を書く基本イメージ

プライマリコンストラクタの基本形は次のとおりです。

C#
public class クラス名(引数リスト)
{
// 引数を使ってプロパティやフィールドを初期化する
}

具体例を見てみましょう。

C#
public class Product(string name, int price)
{
public string Name { get; } = name;
public int Price { get; } = price;
}

このProductクラスは、インスタンス生成時にnamepriceを受け取ります。

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

Console.WriteLine(product.Name); // Keyboard
Console.WriteLine(product.Price); // 5000

型名の横に引数を書くため、「このクラスを作るには何が必要なのか」がクラス宣言を見た時点でわかりやすくなります。

1-3. 従来のコンストラクタよりコードを短く書ける理由

従来のコンストラクタでは、次のようなコードがよく登場します。

C#
public class Customer
{
private readonly string _name;

public Customer(string name)
{
_name = name;
}

public string GetName()
{
return _name;
}
}

プライマリコンストラクタを使うと、同じような処理をより短く書けます。

C#
public class Customer(string name)
{
public string GetName()
{
return name;
}
}

さらに、値をプロパティとして保持したい場合は次のように書けます。

C#
public class Customer(string name)
{
public string Name { get; } = name;
}

従来は「コンストラクタ引数を受け取る」「フィールドやプロパティに代入する」という定型的なコードが必要でした。プライマリコンストラクタでは、引数をクラス本体の中で直接使えるため、同じ意味のコードを少ない行数で表現できます。公式ドキュメントでも、プライマリコンストラクタのパラメータは型本体全体のスコープに入ると説明されています。

1-4. recordだけでなくclass・structでも使える点

C#でプライマリコンストラクタという言葉を聞くと、まずrecordを思い浮かべる人もいるかもしれません。

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

このようなrecordの書き方は、C# 9以降でよく使われてきました。

一方、C# 12では次のように通常のclassstructにもプライマリコンストラクタを書けます。

C#
public class User(string name, int age)
{
public string Name { get; } = name;
public int Age { get; } = age;
}
C#
public readonly struct Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
}

ただし、recordclassstructでは動作が異なります。recordではプライマリコンストラクタのパラメータからプロパティが生成されますが、通常のclassstructでは自動生成されません。ここは非常に重要な違いです。

2. まず押さえたいコンストラクタの基本

2-1. コンストラクタとは何か

コンストラクタとは、クラスや構造体のインスタンスを作成するときに呼び出される特別な処理です。C#では、newを使ってオブジェクトを作成するときにコンストラクタが実行されます。公式ドキュメントでも、コンストラクタはクラスまたは構造体のインスタンスが作成されるときに呼び出されるメソッドだと説明されています。

たとえば、次のコードではnew User("Alice")を実行したタイミングでUserクラスのコンストラクタが呼び出されます。

C#
var user = new User("Alice");

コンストラクタは、オブジェクトを正しい初期状態にするために使われます。

2-2. オブジェクト生成時に初期値を設定する役割

コンストラクタの代表的な役割は、オブジェクト生成時に初期値を設定することです。

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

public User(string name)
{
Name = name;
}
}

この例では、Userオブジェクトを作るときに名前を受け取り、Nameプロパティに設定しています。

C#
var user = new User("Alice");

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

コンストラクタを使うことで、「Userを作るには必ず名前が必要」というルールをクラスに持たせることができます。

2-3. 従来のコンストラクタの書き方

従来のコンストラクタは、クラス名と同じ名前のメソッドのような形で書きます。

C#
public class Book
{
public string Title { get; }
public int Price { get; }

public Book(string title, int price)
{
Title = title;
Price = price;
}
}

ポイントは次の3つです。

C#
public Book(string title, int price)

この部分がコンストラクタです。

通常のメソッドとは異なり、戻り値の型を書きません。voidも書きません。

C#
public void Book(string title, int price)

このように書くと、コンストラクタではなく通常のメソッドになってしまいます。

2-4. 初心者が混乱しやすい「メソッド」との違い

コンストラクタは見た目がメソッドに似ていますが、通常のメソッドとは役割が違います。

通常のメソッドは、オブジェクトを作ったあとに呼び出します。

C#
user.ChangeName("Bob");

一方、コンストラクタはオブジェクトを作るときに呼び出されます。

C#
var user = new User("Alice");

また、通常のメソッドには戻り値の型があります。

C#
public string GetName()
{
return Name;
}

コンストラクタには戻り値の型を書きません。

C#
public User(string name)
{
Name = name;
}

プライマリコンストラクタは、このコンストラクタをより短く書くための記法です。ただし、従来コンストラクタの基本を理解してから使うと、より安全に使い分けられます。

3. C# プライマリコンストラクタの基本的な使い方

3-1. 最小構文で書くプライマリコンストラクタ

C# プライマリコンストラクタの最小構文は次のようになります。

C#
public class Person(string name)
{
}

これだけで、Personクラスはnameを受け取るコンストラクタを持つことになります。

C#
var person = new Person("Alice");

ただし、このままではnameを外部から参照できません。

C#
Console.WriteLine(person.name); // エラー
Console.WriteLine(person.Name); // Nameプロパティを定義していなければエラー

プライマリコンストラクタのパラメータは、あくまでパラメータです。classstructでは、メンバーやプロパティとして自動公開されません。公式ドキュメントでも、プライマリコンストラクタのパラメータはクラスのメンバーではなく、this.paramのようにはアクセスできないと説明されています。

3-2. プロパティの初期化に使う方法

外部から値を参照したい場合は、プロパティに代入します。

C#
public class Person(string name)
{
public string Name { get; } = name;
}

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

C#
var person = new Person("Alice");

Console.WriteLine(person.Name); // Alice

この書き方は、従来コンストラクタの次のコードとほぼ同じ目的で使えます。

C#
public class Person
{
public string Name { get; }

public Person(string name)
{
Name = name;
}
}

単純に引数を受け取り、読み取り専用プロパティへ代入するだけなら、プライマリコンストラクタのほうがすっきりします。

3-3. フィールドの初期化に使う方法

プロパティではなく、フィールドに代入することもできます。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;

public User? FindById(int id)
{
return _repository.FindById(id);
}
}

この例では、IUserRepositoryをコンストラクタで受け取り、_repositoryフィールドに保持しています。

従来の書き方では次のようになります。

C#
public class UserService
{
private readonly IUserRepository _repository;

public UserService(IUserRepository repository)
{
_repository = repository;
}

public User? FindById(int id)
{
return _repository.FindById(id);
}
}

DIでサービスやリポジトリを受け取るクラスでは、このような定型コードが多くなりがちです。プライマリコンストラクタを使うと、ボイラープレートを減らせます。

3-4. メソッド内でパラメータを使う方法

プライマリコンストラクタのパラメータは、メソッド内でも使えます。

C#
public class Greeter(string name)
{
public void SayHello()
{
Console.WriteLine($"Hello, {name}!");
}
}

実行例です。

C#
var greeter = new Greeter("Alice");
greeter.SayHello(); // Hello, Alice!

このように、パラメータを直接メソッド内で参照できます。

ただし、値を長期的に保持する意図があるなら、プロパティやフィールドに代入したほうが読みやすい場合もあります。

C#
public class Greeter(string name)
{
private readonly string _name = name;

public void SayHello()
{
Console.WriteLine($"Hello, {_name}!");
}
}

小さなクラスでは直接パラメータを使っても問題ありませんが、チーム開発では「状態として保持する値はフィールドやプロパティに代入する」と決めたほうがわかりやすいこともあります。

3-5. 実行例で理解するプライマリコンストラクタ

次の例で、プライマリコンストラクタの動きを確認してみましょう。

C#
public class Rectangle(double width, double height)
{
public double Width { get; } = width;
public double Height { get; } = height;

public double GetArea()
{
return width * height;
}
}

実行コードです。

C#
var rectangle = new Rectangle(10, 5);

Console.WriteLine(rectangle.Width); // 10
Console.WriteLine(rectangle.Height); // 5
Console.WriteLine(rectangle.GetArea()); // 50

この例では、widthheightをプロパティの初期化にも、メソッド内の計算にも使っています。

ただし、次のように書くと外部から値を参照できません。

C#
public class Rectangle(double width, double height)
{
public double GetArea()
{
return width * height;
}
}

この場合、rectangle.Widthrectangle.Heightは存在しません。

C#
var rectangle = new Rectangle(10, 5);

Console.WriteLine(rectangle.Width); // エラー

classのプライマリコンストラクタでは、必要に応じて自分でプロパティを定義する必要があります。

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

4-1. 書く場所の違い

従来コンストラクタは、クラス本体の中に書きます。

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

public User(string name)
{
Name = name;
}
}

プライマリコンストラクタは、型名の横に書きます。

C#
public class User(string name)
{
public string Name { get; } = name;
}

この違いにより、クラス宣言を見ただけで「このクラスを作るには何が必要か」がわかりやすくなります。

4-2. コンストラクタ本体がない点

従来コンストラクタには、本体があります。

C#
public User(string name)
{
Name = name;
}

一方、プライマリコンストラクタには、従来のようなコンストラクタ本体を書きません。

C#
public class User(string name)
{
public string Name { get; } = name;
}

では、初期化処理はどこに書くのでしょうか。

基本的には、フィールド初期化子やプロパティ初期化子に書きます。

C#
public class User(string name)
{
public string Name { get; } =
string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("名前は必須です。", nameof(name))
: name;
}

複雑な処理が必要な場合は、静的メソッドに切り出す方法もあります。

C#
public class User(string name)
{
public string Name { get; } = ValidateName(name);

private static string ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

return name;
}
}

ただし、初期化処理が長くなる場合は、従来のコンストラクタのほうが読みやすいこともあります。

4-3. パラメータのスコープの違い

従来コンストラクタの引数は、基本的にコンストラクタ本体の中で使います。

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

public User(string name)
{
Name = name; // nameはこのコンストラクタ内で使う
}
}

プライマリコンストラクタのパラメータは、型本体全体の中で使えます。

C#
public class User(string name)
{
public string Name { get; } = name;

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

この「型本体全体で使える」という点が便利な一方で、初心者には「これはフィールドなのか、プロパティなのか、ただのパラメータなのか」がわかりにくくなる場合があります。公式ドキュメントでも、プライマリコンストラクタのパラメータはスコープが広くてもパラメータとして考えることが重要だと説明されています。

4-4. 暗黙の引数なしコンストラクタへの影響

C#のクラスでは、コンストラクタを1つも定義しない場合、引数なしコンストラクタが暗黙的に用意されます。

C#
public class User
{
}

この場合、次のようにインスタンスを作れます。

C#
var user = new User();

しかし、プライマリコンストラクタを追加すると、クラスでは暗黙の引数なしコンストラクタは生成されません。

C#
public class User(string name)
{
public string Name { get; } = name;
}

この場合、次のコードはエラーになります。

C#
var user = new User(); // エラー

正しくは、必要な引数を渡します。

C#
var user = new User("Alice");

引数なしでも作れるようにしたい場合は、追加コンストラクタを定義します。

C#
public class User(string name)
{
public string Name { get; } = name;

public User() : this("Guest")
{
}
}

追加コンストラクタでは、必ずthis(...)を使ってプライマリコンストラクタを呼び出す必要があります。

4-5. コード量・可読性・保守性の違い

プライマリコンストラクタの大きなメリットは、コード量を減らせることです。

従来の書き方です。

C#
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly ILogger<OrderService> _logger;

public OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
_repository = repository;
_logger = logger;
}
}

プライマリコンストラクタを使うと、次のように書けます。

C#
public class OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
private readonly IOrderRepository _repository = repository;
private readonly ILogger<OrderService> _logger = logger;
}

さらに、メソッド内で直接使う設計なら、フィールド定義も省略できます。

C#
public class OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
public void CreateOrder(Order order)
{
repository.Save(order);
logger.LogInformation("注文を作成しました。");
}
}

ただし、コード量が少ないことと読みやすいことは必ずしも同じではありません。特にチーム内にC# 12に慣れていないメンバーが多い場合、従来コンストラクタのほうが意図が伝わりやすいケースもあります。

5. プライマリコンストラクタが向いているケース

5-1. 単純な初期化だけを行うクラス

プライマリコンストラクタが最も向いているのは、受け取った値をそのままプロパティやフィールドに代入するだけのクラスです。

C#
public class Product(string name, int price)
{
public string Name { get; } = name;
public int Price { get; } = price;
}

従来コンストラクタで書くと、次のようになります。

C#
public class Product
{
public string Name { get; }
public int Price { get; }

public Product(string name, int price)
{
Name = name;
Price = price;
}
}

このような単純な初期化では、プライマリコンストラクタを使うことでコードが短くなり、読みやすくなります。

5-2. DIでサービスを受け取るクラス

ASP.NET CoreなどでDIを使っている場合、サービスクラスやコントローラーでは依存オブジェクトをコンストラクタで受け取ることが多くあります。

C#
public class UserController(
IUserService userService,
ILogger<UserController> logger)
{
public void Show(int id)
{
var user = userService.GetUser(id);
logger.LogInformation("ユーザーを取得しました: {Id}", id);
}
}

従来の書き方では、受け取った依存オブジェクトをフィールドに代入するコードが必要でした。

C#
public class UserController
{
private readonly IUserService _userService;
private readonly ILogger<UserController> _logger;

public UserController(
IUserService userService,
ILogger<UserController> logger)
{
_userService = userService;
_logger = logger;
}
}

プライマリコンストラクタを使うと、DIで受け取る依存関係をクラス宣言にまとめられます。C#のコードスタイル規則IDE0290でも、条件に合うクラスに対してプライマリコンストラクタの使用が提案されることがあります。

5-3. DTOや値を保持する小さな型

DTOや値を保持する小さな型にも、プライマリコンストラクタは向いています。

C#
public class UserDto(int id, string name, string email)
{
public int Id { get; } = id;
public string Name { get; } = name;
public string Email { get; } = email;
}

ただし、DTO用途で値の保持、比較、分解、with式などを使いたい場合は、recordのほうが適していることもあります。

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

recordではパラメータからプロパティが生成されるため、単純なデータ保持には非常に簡潔です。一方、通常のclassで独自の振る舞いを持たせたい場合は、プライマリコンストラクタ付きのclassが選択肢になります。

5-4. 継承元クラスに引数を渡したい場合

プライマリコンストラクタは、基底クラスのコンストラクタに引数を渡す場合にも使えます。

C#
public class Animal(string name)
{
public string Name { get; } = name;
}

public class Dog(string name, string breed) : Animal(name)
{
public string Breed { get; } = breed;
}

この例では、Dogのプライマリコンストラクタで受け取ったnameを、基底クラスAnimalのコンストラクタに渡しています。

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

Console.WriteLine(dog.Name); // Pochi
Console.WriteLine(dog.Breed); // Shiba

公式ドキュメントでも、プライマリコンストラクタのパラメータはbase()コンストラクタ呼び出しの引数として使えると説明されています。

5-5. ボイラープレートを減らしたい場合

ボイラープレートとは、毎回ほとんど同じ形で書く定型コードのことです。

たとえば、次のようなコードは多くのサービスクラスで繰り返し書かれます。

C#
private readonly IUserRepository _repository;

public UserService(IUserRepository repository)
{
_repository = repository;
}

プライマリコンストラクタを使えば、こうした定型的な代入コードを減らせます。

C#
public class UserService(IUserRepository repository)
{
public User? Find(int id)
{
return repository.Find(id);
}
}

ただし、値を明示的にフィールドとして持ちたい場合は、次のように書くのもよいです。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;

public User? Find(int id)
{
return _repository.Find(id);
}
}

コード量を減らすことだけを目的にするのではなく、読みやすさとのバランスで判断することが大切です。

6. プライマリコンストラクタの注意点

6-1. パラメータは自動でプロパティになるわけではない

C# プライマリコンストラクタで最も重要な注意点は、classstructではパラメータが自動でプロパティにならないことです。

次のコードを見てください。

C#
public class Person(string name)
{
}

このクラスはnameを受け取れます。

C#
var person = new Person("Alice");

しかし、次のようには書けません。

C#
Console.WriteLine(person.Name); // エラー

Nameプロパティを定義していないからです。

正しくは、次のようにプロパティを自分で定義します。

C#
public class Person(string name)
{
public string Name { get; } = name;
}

この点は、recordと大きく違います。recordではプライマリコンストラクタのパラメータからプロパティが生成されますが、通常のclassstructでは生成されません。

6-2. 値を保持したい場合はプロパティやフィールドに代入する

受け取った値をオブジェクトの状態として保持したい場合は、プロパティやフィールドに代入しましょう。

外部に公開したい場合はプロパティです。

C#
public class User(string name)
{
public string Name { get; } = name;
}

内部だけで使いたい場合はフィールドです。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;
}

一時的に使うだけなら、直接パラメータを参照しても構いません。

C#
public class TaxCalculator(decimal taxRate)
{
public decimal Calculate(decimal price)
{
return price * (1 + taxRate);
}
}

ただし、直接パラメータを使う設計にすると、後から読んだ人が「これはフィールドなのか、コンストラクタ引数なのか」を確認する必要があります。チームで統一した書き方を決めておくと安全です。

6-3. 追加コンストラクタはthisでプライマリコンストラクタを呼ぶ必要がある

プライマリコンストラクタを持つクラスに別のコンストラクタを追加する場合、その追加コンストラクタはthis(...)でプライマリコンストラクタを呼び出す必要があります。公式ドキュメントでも、他のコンストラクタはプライマリコンストラクタを通して呼び出す必要があると説明されています。

正しい例です。

C#
public class User(string name)
{
public string Name { get; } = name;

public User() : this("Guest")
{
}
}

誤った例です。

C#
public class User(string name)
{
public string Name { get; } = name;

public User()
{
// エラー
}
}

プライマリコンストラクタのパラメータが必ず初期化されるようにするため、追加コンストラクタはプライマリコンストラクタへ処理をつなげる必要があります。

6-4. 複雑な初期化処理には向かない場合がある

プライマリコンストラクタは、単純な初期化を短く書くのに向いています。

C#
public class Product(string name, int price)
{
public string Name { get; } = name;
public int Price { get; } = price;
}

一方、次のような複雑な処理が必要な場合は、従来コンストラクタのほうが読みやすいことがあります。

C#
public class Product
{
public string Name { get; }
public int Price { get; }
public bool IsDiscounted { get; }

public Product(string name, int price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("商品名は必須です。", nameof(name));
}

if (price < 0)
{
throw new ArgumentOutOfRangeException(nameof(price), "価格は0以上である必要があります。");
}

Name = name;
Price = price;
IsDiscounted = price >= 10000;
}
}

このように、バリデーションや条件分岐が多い場合、従来コンストラクタのほうが処理の流れを追いやすくなります。

プライマリコンストラクタでも書けますが、初期化子が複雑になりすぎるなら無理に使う必要はありません。

6-5. フィールドとパラメータの二重保持に注意する

プライマリコンストラクタでは、パラメータを直接メソッド内で使うことも、フィールドに代入して使うこともできます。

しかし、両方を混ぜるとわかりにくくなる場合があります。

C#
public class Sample(string name)
{
private readonly string _name = name;

public string Name => name;
}

このコードでは、_nameにもnameにも同じ値が保持される可能性があります。実装によっては意図せず重複した状態を持つ形になり、可読性が下がります。

基本的には、次のどちらかに統一したほうがわかりやすいです。

プロパティに保持する例です。

C#
public class Sample(string name)
{
public string Name { get; } = name;
}

フィールドに保持する例です。

C#
public class Sample(string name)
{
private readonly string _name = name;

public string GetName()
{
return _name;
}
}

「パラメータを直接使う」のか、「フィールドやプロパティに代入して使う」のかを、クラス内で混在させすぎないようにしましょう。

6-6. structで使う場合のdefault値に注意する

structでもプライマリコンストラクタを使えます。

C#
public readonly struct Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
}

次のように作れば問題ありません。

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

Console.WriteLine(point.X); // 10
Console.WriteLine(point.Y); // 20

ただし、structにはdefault値があります。

C#
Point point = default;

Console.WriteLine(point.X); // 0
Console.WriteLine(point.Y); // 0

defaultでは、各フィールドが0やnullなどの既定値になります。公式ドキュメントでも、structdefault値に設定されると、ランタイムはメモリを0で初期化すると説明されています。

そのため、structで「必ずコンストラクタを通って検証済みの値になる」と考えるのは危険です。structを設計するときは、default状態でも問題が起きにくいようにする必要があります。

7. よくあるエラーと解決方法

7-1. パラメータ名とメンバー名が衝突する

プライマリコンストラクタでは、パラメータ名とメンバー名が似ていると混乱しやすくなります。

C#
public class User(string name)
{
public string name { get; } = name;
}

C#では大文字小文字を区別するため、技術的にはnameというプロパティも書けます。しかし、読みづらくなるためおすすめしません。

一般的には、クラスや構造体のプライマリコンストラクタ引数はキャメルケースにし、プロパティはパスカルケースにします。MicrosoftのC#コーディング規約でも、classstructのプライマリコンストラクタパラメータにはcamel caseを使うとされています。

C#
public class User(string name)
{
public string Name { get; } = name;
}

フィールドに代入する場合は、フィールド名に_を付ける書き方もよく使われます。

C#
public class User(string name)
{
private readonly string _name = name;
}

7-2. 追加コンストラクタでthis呼び出しを忘れる

よくあるエラーの1つが、追加コンストラクタでthis(...)を忘れるケースです。

誤った例です。

C#
public class User(string name)
{
public string Name { get; } = name;

public User()
{
}
}

このような追加コンストラクタは、プライマリコンストラクタに処理をつなげていないためエラーになります。

正しい例です。

C#
public class User(string name)
{
public string Name { get; } = name;

public User() : this("Guest")
{
}
}

追加コンストラクタを書くときは、「最終的にプライマリコンストラクタが呼ばれるか」を必ず確認しましょう。

7-3. プロパティに代入していないため値を参照できない

次のコードは、一見するとNameを持っているように見えるかもしれません。

C#
public class User(string name)
{
}

しかし、nameはプロパティではありません。

C#
var user = new User("Alice");

Console.WriteLine(user.Name); // エラー

外部から参照したい場合は、プロパティを定義します。

C#
public class User(string name)
{
public string Name { get; } = name;
}

内部だけで使う場合は、メソッド内で直接参照できます。

C#
public class User(string name)
{
public void Print()
{
Console.WriteLine(name);
}
}

recordと同じ感覚でclassにプライマリコンストラクタを書くと、このミスが起きやすいので注意しましょう。

7-4. readonlyフィールドへの代入で混乱する

従来コンストラクタでは、readonlyフィールドへの代入はコンストラクタ本体で行います。

C#
public class UserService
{
private readonly IUserRepository _repository;

public UserService(IUserRepository repository)
{
_repository = repository;
}
}

プライマリコンストラクタでは、フィールド初期化子で代入できます。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;
}

ここで混乱しやすいのは、「コンストラクタ本体がないのにreadonlyフィールドへ代入してよいのか」という点です。

フィールド初期化子はインスタンス生成時に実行されるため、この書き方は問題ありません。

ただし、次のようにメソッド内でreadonlyフィールドへ代入することはできません。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository;

public void Initialize()
{
_repository = repository; // エラー
}
}

readonlyフィールドは、宣言時またはコンストラクタで初期化するものだと理解しておきましょう。

7-5. IDE0290の提案が出たときの判断基準

Visual StudioやIDEで、IDE0290という提案が表示されることがあります。

IDE0290は、従来コンストラクタをプライマリコンストラクタに置き換えられる場合に表示されるコードスタイルの提案です。公式ドキュメントでは、IDE0290は「プライマリコンストラクタを使用する」ルールで、条件に合うクラスに対してプライマリコンストラクタの使用を提案すると説明されています。

たとえば、次のようなクラスでは提案が出ることがあります。

C#
public class UserService
{
private readonly IUserRepository _repository;

public UserService(IUserRepository repository)
{
_repository = repository;
}
}

置き換えると次のようになります。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;
}

ただし、IDEの提案は必ず従うべきものではありません。

次のような場合は、従来コンストラクタのままでもよいでしょう。

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

public User(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

Name = name;
}
}

判断基準は、「変換後のコードのほうが本当に読みやすいか」です。コード量が少なくなっても、チーム内で理解しづらくなるなら、従来コンストラクタを選ぶのも正しい判断です。

8. recordのプライマリコンストラクタとの違い

8-1. recordではパラメータからプロパティが生成される

recordでは、プライマリコンストラクタのパラメータからプロパティが自動生成されます。

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

この場合、次のように使えます。

C#
var person = new Person("Alice", 20);

Console.WriteLine(person.Name); // Alice
Console.WriteLine(person.Age); // 20

NameAgeのプロパティを自分で書いていないにもかかわらず、外部から参照できます。

公式ドキュメントでも、recordでプライマリコンストラクタを宣言すると、コンパイラがプライマリコンストラクタパラメータに対応するpublicプロパティを生成すると説明されています。

8-2. classやstructでは自動プロパティ生成されない

一方、通常のclassではプロパティは自動生成されません。

C#
public class Person(string name, int age)
{
}

このクラスでは、次のコードはエラーになります。

C#
var person = new Person("Alice", 20);

Console.WriteLine(person.Name); // エラー
Console.WriteLine(person.Age); // エラー

正しくは、自分でプロパティを定義します。

C#
public class Person(string name, int age)
{
public string Name { get; } = name;
public int Age { get; } = age;
}

structでも同じです。

C#
public readonly struct Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
}

classstructのプライマリコンストラクタは、あくまで「型本体で使えるコンストラクタパラメータ」を宣言する機能だと考えると理解しやすくなります。

8-3. recordとclassで書き方が似ていても意味が違う点

次の2つのコードは、見た目がとても似ています。

C#
public record User(string Name);
C#
public class User(string name)
{
}

しかし、意味は大きく違います。

recordの場合、Nameプロパティが生成されます。

C#
var user = new User("Alice");
Console.WriteLine(user.Name);

通常のclassの場合、プロパティは生成されません。

C#
var user = new User("Alice");
Console.WriteLine(user.Name); // エラー

通常のclassで同じように使いたいなら、次のように書く必要があります。

C#
public class User(string name)
{
public string Name { get; } = name;
}

この違いを理解しておかないと、「C# プライマリコンストラクタを書いたのにプロパティがない」という混乱が起きます。

8-4. 初心者が混同しやすいポイント

初心者が特に混同しやすいポイントは、次の4つです。

1つ目は、recordのパラメータはプロパティになるが、classのパラメータはプロパティにならないことです。

C#
public record A(string Name); // Nameプロパティができる

public class B(string name)
{
// Nameプロパティは自動ではできない
}

2つ目は、recordではパラメータ名をパスカルケースにすることが多いのに対し、classstructではキャメルケースにすることが推奨される点です。Microsoftのコーディング規約でも、record型ではPascal case、classstructのプライマリコンストラクタパラメータではcamel caseを使う例が示されています。

C#
public record Person(string FirstName, string LastName);

public class Person(string firstName, string lastName)
{
public string FirstName { get; } = firstName;
public string LastName { get; } = lastName;
}

3つ目は、recordには値の比較やwith式など、データ中心の型に便利な機能がある点です。

4つ目は、classのプライマリコンストラクタは、従来コンストラクタを短く書くための機能であり、recordと同じデータ型生成機能ではないという点です。

9. 実践コードで比較するプライマリコンストラクタ

9-1. 従来コンストラクタで書いた例

まず、従来コンストラクタで書いたサービスクラスを見てみましょう。

C#
public interface IEmailSender
{
void Send(string to, string subject, string body);
}

public class NotificationService
{
private readonly IEmailSender _emailSender;

public NotificationService(IEmailSender emailSender)
{
_emailSender = emailSender;
}

public void Notify(string email, string message)
{
_emailSender.Send(
email,
"お知らせ",
message);
}
}

このコードはわかりやすいですが、コンストラクタで受け取った値をフィールドに代入するだけの定型コードがあります。

C#
private readonly IEmailSender _emailSender;

public NotificationService(IEmailSender emailSender)
{
_emailSender = emailSender;
}

この部分をプライマリコンストラクタで短くできます。

9-2. プライマリコンストラクタに書き換えた例

プライマリコンストラクタに書き換えると、次のようになります。

C#
public class NotificationService(IEmailSender emailSender)
{
public void Notify(string email, string message)
{
emailSender.Send(
email,
"お知らせ",
message);
}
}

かなり短くなりました。

フィールドとして明示的に保持したい場合は、次のように書きます。

C#
public class NotificationService(IEmailSender emailSender)
{
private readonly IEmailSender _emailSender = emailSender;

public void Notify(string email, string message)
{
_emailSender.Send(
email,
"お知らせ",
message);
}
}

どちらがよいかは、チームの方針やクラスの複雑さによります。

小さなクラスならパラメータを直接使っても読みやすいです。

C#
public class NotificationService(IEmailSender emailSender)
{
public void Notify(string email, string message)
{
emailSender.Send(email, "お知らせ", message);
}
}

長いクラスや複数メソッドで繰り返し使う場合は、フィールドに代入したほうが意図が伝わりやすいことがあります。

C#
public class NotificationService(IEmailSender emailSender)
{
private readonly IEmailSender _emailSender = emailSender;

public void Notify(string email, string message)
{
_emailSender.Send(email, "お知らせ", message);
}
}

9-3. DIを使ったクラスの例

ASP.NET Core風のサービスクラスで考えてみます。

従来コンストラクタです。

C#
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;

public OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}

public void Create(Order order)
{
_orderRepository.Save(order);
_logger.LogInformation("注文を保存しました。");
}
}

プライマリコンストラクタを使うと、次のように書けます。

C#
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
public void Create(Order order)
{
orderRepository.Save(order);
logger.LogInformation("注文を保存しました。");
}
}

この書き方では、依存関係がクラス宣言部分に集約されています。

ただし、クラスが大きくなる場合は、明示的にフィールドを用意する書き方も有効です。

C#
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ILogger<OrderService> _logger = logger;

public void Create(Order order)
{
_orderRepository.Save(order);
_logger.LogInformation("注文を保存しました。");
}
}

DIでプライマリコンストラクタを使う場合は、「直接パラメータを使うのか」「フィールドに代入するのか」をプロジェクト内で統一すると、コードレビューがしやすくなります。

9-4. 継承を使った例

継承とプライマリコンストラクタを組み合わせる例です。

C#
public abstract class Entity(int id)
{
public int Id { get; } = id;
}

public class User(int id, string name) : Entity(id)
{
public string Name { get; } = name;
}

使い方です。

C#
var user = new User(1, "Alice");

Console.WriteLine(user.Id); // 1
Console.WriteLine(user.Name); // Alice

Userクラスのプライマリコンストラクタで受け取ったidを、基底クラスEntityへ渡しています。

従来コンストラクタで書くと次のようになります。

C#
public abstract class Entity
{
public int Id { get; }

protected Entity(int id)
{
Id = id;
}
}

public class User : Entity
{
public string Name { get; }

public User(int id, string name) : base(id)
{
Name = name;
}
}

プライマリコンストラクタを使うと、基底クラスへの引数受け渡しも短く書けます。

9-5. 書き換え前後で読みやすさを比較する

従来コンストラクタのメリットは、処理の流れが明確なことです。

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

public User(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

Name = name;
}
}

プライマリコンストラクタで書くと、次のようになります。

C#
public class User(string name)
{
public string Name { get; } = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("名前は必須です。", nameof(name))
: name;
}

短くはなりましたが、初心者には少し読みにくいかもしれません。

一方、単純な代入だけなら、プライマリコンストラクタのほうが読みやすいです。

C#
public class User(string name)
{
public string Name { get; } = name;
}

つまり、判断基準は「短くなるか」だけではありません。

単純な初期化ならプライマリコンストラクタ、条件分岐や複雑な検証があるなら従来コンストラクタ、という使い分けが基本です。

10. プライマリコンストラクタを使うべきかの判断基準

10-1. 使うとコードがシンプルになるケース

次のようなケースでは、プライマリコンストラクタを使うとコードがシンプルになります。

受け取った値をそのままプロパティに代入するだけのクラスです。

C#
public class Category(string name)
{
public string Name { get; } = name;
}

DIで受け取った依存関係を使うだけのサービスです。

C#
public class ReportService(IReportRepository repository)
{
public Report GetReport(int id)
{
return repository.Find(id);
}
}

基底クラスへ引数を渡すだけの派生クラスです。

C#
public class AdminUser(int id, string name) : User(id)
{
public string Name { get; } = name;
}

このようなケースでは、従来コンストラクタよりもプライマリコンストラクタのほうが簡潔です。

10-2. 従来コンストラクタのほうが読みやすいケース

次のような場合は、従来コンストラクタのほうが読みやすいことがあります。

バリデーションが多い場合です。

C#
public class Product
{
public string Name { get; }
public int Price { get; }

public Product(string name, int price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("商品名は必須です。", nameof(name));
}

if (price < 0)
{
throw new ArgumentOutOfRangeException(nameof(price));
}

Name = name;
Price = price;
}
}

初期化時に複数の処理を順番に実行したい場合です。

C#
public class Application
{
private readonly Config _config;

public Application(string configPath)
{
var text = File.ReadAllText(configPath);
_config = Config.Parse(text);
ValidateConfig(_config);
}

private static void ValidateConfig(Config config)
{
// 検証処理
}
}

例外処理やログ出力など、初期化時の処理が長くなる場合も、従来コンストラクタのほうが自然です。

10-3. チーム開発で導入する際の注意点

チーム開発でC# プライマリコンストラクタを導入する場合は、いきなり全コードを置き換えるのではなく、ルールを決めることが重要です。

たとえば、次のようなルールが考えられます。

単純な代入だけのクラスでは使う。

C#
public class User(string name)
{
public string Name { get; } = name;
}

バリデーションが複雑な場合は従来コンストラクタを使う。

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

public User(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

Name = name;
}
}

DIクラスでは、直接パラメータを使うか、フィールドに代入するかを統一する。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;
}

IDE0290の提案を自動的にすべて適用するのではなく、読みやすさを確認してから採用する。

こうしたルールがないまま導入すると、同じプロジェクト内で書き方がバラバラになり、かえって保守性が下がる可能性があります。

10-4. 初心者がまず覚えるべき使い分け

初心者は、まず次の使い分けを覚えるとよいです。

単純に値を受け取ってプロパティに入れるだけなら、プライマリコンストラクタを使う。

C#
public class Person(string name)
{
public string Name { get; } = name;
}

コンストラクタ内で複数行の処理をしたいなら、従来コンストラクタを使う。

C#
public class Person
{
public string Name { get; }

public Person(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

Name = name;
}
}

データを保持するだけの型なら、recordも検討する。

C#
public record Person(string Name);

プライマリコンストラクタは便利ですが、従来コンストラクタを置き換えるためだけのものではありません。状況に応じて、最も読みやすい書き方を選ぶことが大切です。

11. C# プライマリコンストラクタに関するよくある質問

11-1. C#のどのバージョンから使える?

通常のclassstructでプライマリコンストラクタを使えるようになったのはC# 12からです。Microsoftの公式ドキュメントでも、C# 12で任意のclassstructにプライマリコンストラクタを作成できるようになったと説明されています。

なお、recordではC# 9以降から似たようなプライマリコンストラクタの書き方が使われていました。

11-2. .NET Frameworkでも使える?

C#の言語バージョンは、プロジェクトのターゲットフレームワークによって既定値が決まります。Microsoftの言語バージョン管理のドキュメントでは、.NET 8.xの既定はC# 12、.NET Frameworkの既定はC# 7.3とされています。

そのため、基本的には.NET 8以降のプロジェクトで使うのが自然です。

プロジェクトファイルでLangVersionを変更すれば新しい構文を使える場合もありますが、公式ドキュメントでは、ターゲットフレームワークに関連付けられたバージョンより新しいC#言語バージョンを使うことはサポートされないと注意されています。

実務では、.NET Frameworkプロジェクトに無理にC# 12機能を導入するより、.NET 8以降への移行可否も含めて検討するのが安全です。

11-3. プライマリコンストラクタは必ず使うべき?

必ず使う必要はありません。

プライマリコンストラクタは、単純な初期化を短く書きたい場合に便利な機能です。

C#
public class User(string name)
{
public string Name { get; } = name;
}

しかし、複雑なバリデーションや初期化処理がある場合は、従来コンストラクタのほうが読みやすいこともあります。

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

public User(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

Name = name;
}
}

「新しい機能だから使う」のではなく、「使うことでコードが読みやすくなるか」を基準にしましょう。

11-4. コンストラクタの中に処理を書きたい場合はどうする?

プライマリコンストラクタには、従来のようなコンストラクタ本体がありません。

簡単な検証なら、プロパティ初期化子で書けます。

C#
public class User(string name)
{
public string Name { get; } = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("名前は必須です。", nameof(name))
: name;
}

少し複雑なら、静的メソッドに切り出せます。

C#
public class User(string name)
{
public string Name { get; } = ValidateName(name);

private static string ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

return name;
}
}

ただし、処理が長くなるなら、従来コンストラクタを使ったほうが読みやすいです。

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

public User(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}

Name = name;
}
}

プライマリコンストラクタにこだわりすぎないことが大切です。

11-5. プロパティとフィールドのどちらに代入すべき?

外部から参照したい値はプロパティに代入します。

C#
public class User(string name)
{
public string Name { get; } = name;
}

クラス内部だけで使う依存オブジェクトや状態は、フィールドに代入します。

C#
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repository = repository;
}

値を外部に公開しないなら、メソッド内でパラメータを直接使うこともできます。

C#
public class UserService(IUserRepository repository)
{
public User? Find(int id)
{
return repository.Find(id);
}
}

初心者のうちは、次の基準で考えるとわかりやすいです。

外部に見せたい値はプロパティ。

内部だけで使う値はフィールド。

小さなクラスで一時的に使うだけならパラメータを直接参照。

ただし、プロジェクト内で書き方が混在しすぎると読みにくくなるため、チームのコーディング規約に合わせることも重要です。

まとめ

C# プライマリコンストラクタは、C# 12で通常のclassstructにも導入された、コンストラクタを短く書くための便利な記法です。

従来のコンストラクタでは、型の中にコンストラクタ本体を書いて、受け取った引数をフィールドやプロパティに代入していました。

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

public User(string name)
{
Name = name;
}
}

プライマリコンストラクタを使うと、型名の横に引数を書けます。

C#
public class User(string name)
{
public string Name { get; } = name;
}

単純な初期化だけなら、コードが短くなり、クラスに必要な値もわかりやすくなります。

ただし、注意点もあります。

classstructでは、プライマリコンストラクタのパラメータは自動でプロパティになりません。

C#
public class User(string name)
{
}

このコードを書いても、user.Nameは使えません。

外部から参照したい場合は、プロパティを自分で定義する必要があります。

C#
public class User(string name)
{
public string Name { get; } = name;
}

また、追加コンストラクタを書く場合は、this(...)でプライマリコンストラクタを呼び出す必要があります。

C#
public class User(string name)
{
public string Name { get; } = name;

public User() : this("Guest")
{
}
}

recordではパラメータからプロパティが生成されますが、通常のclassstructでは生成されない点も重要です。

C#
public record User(string Name); // Nameプロパティが生成される

public class UserClass(string name)
{
public string Name { get; } = name; // 自分で定義する
}

C# プライマリコンストラクタは、単純な初期化、DIでの依存関係受け取り、小さな型、基底クラスへの引数受け渡しなどに向いています。一方で、複雑なバリデーションや長い初期化処理がある場合は、従来コンストラクタのほうが読みやすいこともあります。

初心者はまず、「単純な初期化ならプライマリコンストラクタ」「複雑な処理があるなら従来コンストラクタ」「データ中心ならrecordも検討」という使い分けを覚えるとよいでしょう。