C# 2026年のプロパティ宣言——fieldキーワードとget/setの書き方まとめ
C#のプロパティ宣言は、バージョンを追うごとにどんどん短く書けるようになっている。古いコードを読んでいると、バッキングフィールドを自分で宣言してgetとsetでそれを返すだけ、という10行近いボイラープレートが普通に出てくる。2026年時点の書き方と比べると、同じことをやっているのに別の言語みたいに見える。
この記事では、C# 14(.NET 10)までの機能を踏まえて、2026年時点でプロパティをどう書くべきかを整理した。特にfieldキーワードは実際に使い始めたら手放せなくなったので、そこを中心に書いている。
古い書き方——バッキングフィールドを自分で管理する
昔からあるパターンはこれだ。バリデーションや副作用が必要なプロパティは、プライベートフィールドを自分で宣言して、getとsetの中で操作する。
public class User
{
private string _name;
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("名前は空にできません");
_name = value;
}
}
}
動くし読めるけど、_nameという変数がNameプロパティのためだけに存在していて、それが当たり前のように積み重なっていく。クラスが大きくなると、どのフィールドがどのプロパティに対応しているかを目で追う必要が出てくる。
C# 14のfieldキーワード——バッキングフィールドが不要になる
.NET 10(C# 14)で追加されたfieldキーワードを使うと、コンパイラが生成するバッキングフィールドにアクセサの中から直接アクセスできる。上のコードは次のように書き直せる。
public class User
{
public string Name
{
get => field;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("名前は空にできません");
field = value;
}
}
}
_nameの宣言が消えた。fieldはコンパイラが自動生成するバッキングフィールドへの参照で、外から直接触れない。オートプロパティとカスタムアクセサの中間点みたいな位置づけで、「単純に値を返すだけじゃないけど、専用のフィールドを宣言するほどでもない」ケースにちょうど合う。
片方だけカスタムにすることもできる
setだけロジックを入れて、getはそのまま返す、という使い方が実際には多い。
public string Email
{
get;
set
{
if (!value.Contains('@'))
throw new FormatException("メールアドレスの形式が不正です");
field = value.ToLowerInvariant();
}
}
getはオートプロパティのまま、setだけカスタムにできる。これがfieldキーワードの使い方としていちばん出番が多いんじゃないかと思っている。
initアクセサ——オブジェクト初期化時だけ書き込みを許可する
C# 9で追加されたinitは、コンストラクタやオブジェクト初期化子から設定できるけど、その後は変更できないプロパティを作れる。setの代わりにinitと書くだけだ。
public class Order
{
public Guid Id { get; init; } = Guid.NewGuid();
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public string CustomerId { get; init; }
}
// 初期化子でセット可能
var order = new Order { CustomerId = "C001" };
// これはコンパイルエラー
order.CustomerId = "C002";
レコード型と組み合わせることが多いけど、普通のクラスで「作ったあとは変えない値」を持ちたいときにも使える。
requiredモディファイア——初期化を強制する
C# 11で入ったrequiredは、オブジェクト初期化子でそのプロパティを設定しないとコンパイルエラーにしてくれる。
public class Product
{
public required string Name { get; init; }
public required decimal Price { get; init; }
public string Description { get; init; } // 任意
}
// Nameを省略するとコンパイルエラー
var p = new Product { Price = 1000 }; // エラー: Nameが設定されていない
// 正しい使い方
var p = new Product { Name = "コーヒー豆", Price = 1000 };
コンストラクタを書かなくてもプロパティの初期化漏れを防げる。DTO・モデルクラスで特に重宝する。
プライマリコンストラクタ——C# 12で普通のクラスにも
C# 12でレコード型だけでなく通常のクラスにもプライマリコンストラクタが使えるようになった。DIでよくある「コンストラクタでサービスを受け取ってフィールドに代入する」パターンが短く書ける。
// 以前
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly ILogger _logger;
public OrderService(IOrderRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
}
// C# 12以降
public class OrderService(IOrderRepository repository, ILogger logger)
{
public async Task GetAsync(Guid id)
{
logger.LogInformation("Getting order {Id}", id);
return await repository.GetByIdAsync(id);
}
}
フィールド宣言が消えてコンストラクタも消えた。パラメータはクラス全体のスコープで使える。ただし、プライマリコンストラクタのパラメータはプロパティではないので、外から参照させたい場合は別途プロパティとして定義する必要がある点は注意だ。
2026年時点での使い分け
整理するとこうなる。
| パターン | 使う場面 |
|---|---|
{ get; set; } | 単純なプロパティ。バリデーションなし |
{ get; init; } | 初期化後は変更させたくないプロパティ |
required { get; init; } | 初期化必須 + 変更不可。DTOやモデル |
fieldキーワード | setにバリデーション・変換が必要。バッキングフィールドは不要 |
| バッキングフィールド手動宣言 | フィールドを他のメソッドからも参照する場合 |
fieldキーワードが追加されたことで、手動バッキングフィールドが必要なケースはかなり限られてきた。大半のケースはfieldで書けるし、その方が宣言が少なくてクラスがスッキリする。
まとめ
- C# 14の
fieldキーワードでバッキングフィールド宣言が不要になった。setのバリデーションにfield = valueと書くだけ init(C# 9)で初期化後に変更不可のプロパティを宣言できるrequired(C# 11)でプロパティの初期化漏れをコンパイル時に検出できる- プライマリコンストラクタ(C# 12)でDIのボイラープレートを大幅に削減できる