プロパティは、それを使うコードからはフィールドのように見えますが(order.Total、order.Total = 5)、実体はメソッドの組です。値を返す get アクセサーと、値を受け取る set アクセサーです。この間接性によって、クラスは呼び出し側の使い方を変えずに、値を確認したり、計算したり、書き込みを拒否したりできます。
フィールドとプロパティ
公開フィールドは、オブジェクトの格納場所への直接のアクセスを与えてしまいます。誰でも何でも入れられます。
public class Account
{
public decimal Balance; // any code can write -500 here
}
プロパティは、呼び出し側と格納場所の間にコードを置きます。これは手書きの完全な形で、privateなバッキングフィールドと、両方のアクセサーを持つプロパティです。set の中では、キーワード value が代入される値です。
出力:
250
Rejected a negative balance
250
検証以外でもプロパティが重要なのは、.NETのエコシステムの大部分がプロパティを扱い、フィールドを無視するからです。インターフェースはプロパティを宣言できてもインスタンスフィールドは宣言できず、WPFやMAUIのデータバインディングはプロパティにバインドし、System.Text.Json は既定で公開プロパティをシリアル化してフィールドを飛ばします。
自動実装プロパティ
アクセサーがフィールドを読み書きするだけなら、コンパイラに書かせます。{ get; set; } は自動実装プロパティで、コンパイラが隠れたバッキングフィールドを生成してくれます。
出力:
Notebook: 9.99, 10 left
フィールドのように見えるのに、なぜわざわざ使うのでしょうか。公開フィールドを後からプロパティに変えると、それを使うコンパイル済みのコードにとって(ref と out の引数にとっても)破壊的変更になりますが、自動実装プロパティを検証付きの完全なプロパティに変えても、呼び出し側からは見えないからです。{ get; set; } で始めておけば、その選択肢をコストなしで残せます。
private setと読み取り専用のプロパティ
ほとんどの状態は、誰でも読めて、オブジェクト自身だけが変更できるべきです。setterにより狭いアクセス修飾子を付けます。
出力:
Ines: 2 items, total 15.75
ここには3種類の読み取り専用があります。
{ get; private set; }:クラスはいつでも変更でき、外部のコードは変更できません。{ get; }(読み取り専用の自動実装プロパティ、C# 6):代入できるのはコンストラクターか初期化子の中だけです。構築後は、クラス自身も含めて誰も変更できません。不変のプロパティを作る方法です。=> expression(計算されるプロパティ):格納場所がまったくありません。式は読まれるたびに実行されるので、Totalは常にリストと一致します。
計算されるプロパティは、低コストで副作用のないものにすべきです。呼び出し側はループの中やデバッガーで気軽にプロパティを読むからです。値の取得に実際の処理(データベースのクエリ、大きな計算)が必要なら、CalculateTotal() のようなメソッドにして、コストが見えるようにします。
式形式のアクセサー
アクセサーが1行の完全なプロパティでは、アクセサーごとに => を使えます。
private string title;
public string Title
{
get => title;
set => title = value?.Trim() ?? "";
}
public decimal Total => SumPrices(); はgetterだけを持つプロパティの省略形で、get => ... は、setterも持つプロパティの中での同じ考え方です。
getterとsetterの処理:通知と遅延評価の値
アクセサーはメソッドなので、値を格納する以上のことができます。よくあるパターンは2つで、setterから変更の通知を発生させることと、最初に読まれたときに値を計算してキャッシュすることです。
出力:
(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark
Changed はイベントで、このsetterのパターンは、WPFやMAUIでの INotifyPropertyChanged の実装そのものです。nameof(Theme) を使えば、プロパティの名前が変わっても文字列が一致したままになります。
無限再帰の間違い
最もよくあるプロパティのバグは、バッキングフィールドではなくプロパティ自身に代入するsetterです。
public string Name
{
get { return Name; } // calls get again, forever
set { Name = value; } // calls set again, forever
}
各アクセサーが自分自身を呼び、プログラムはキャッチできない StackOverflowException で終了します。直し方は、別のフィールド(小文字の name)を読み書きするか、自動実装プロパティを使うことです。C# 14では、まさにこのために field キーワードが追加されました。アクセサーの中の field はコンパイラが生成したバッキングフィールドを指すので、フィールドを宣言しなくても set => field = value.Trim(); が動きます。
initアクセサー(C# 9)
読み取り専用のプロパティでは、すべての値をコンストラクターを通して渡す必要があります。C# 9では、オブジェクトの作成中、つまりコンストラクターかオブジェクト初期化子の中でだけ許されるsetterの init が追加されました。
public class Product
{
public string Sku { get; init; }
public decimal Price { get; init; }
}
var p = new Product { Sku = "MUG-01", Price = 8.50m }; // fine: during creation
p.Price = 4m; // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
// in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor
結果として、初期化子の構文を持つ不変のオブジェクトが得られます。これはまさにレコードクラスが位置指定のプロパティに使っているものです。そのようなオブジェクトを「変更」するには新しいものを作ります。レコードなら with で1行で済みます。
C# 7で最も近いものは、読み取り専用のプロパティとコンストラクターの引数の組み合わせです。
public class Product
{
public string Sku { get; }
public decimal Price { get; }
public Product(string sku, decimal price) { Sku = sku; Price = price; }
}
requiredメンバー(C# 11)
オブジェクト初期化子は、呼び出し側に何かを設定させることを強制できません。Sku が空であってはならない場合でも、new Product { } はコンパイルできてしまいます。C# 11では required 修飾子が追加されました。
public class User
{
public required string Email { get; init; }
public string DisplayName { get; init; } = "";
}
var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" }; // error CS9035: Required member 'User.Email'
// must be set in the object initializer or attribute constructor.
required は set とも init とも組み合わせられます。必須のメンバーを自分で設定するコンストラクターは、[SetsRequiredMembers] 属性でそれを示せるので、そのコンストラクターを使う呼び出し側に再び設定を求めることはありません。
よくある間違い
- アクセサーでの再帰。
set { Name = value; }は自分自身を呼びます。バッキングフィールドか自動実装プロパティを使います。 - 高コストな、または副作用のあるgetter。 読む人はプロパティの読み取りが速く、何度繰り返しても安全だと期待します。処理にはメソッドを使います。
- 「とりあえず」の公開フィールド。 フィールドを後からプロパティに変えるとバイナリ互換性が壊れ、シリアライザーは既定でフィールドを無視します。自動実装プロパティで始めます。
- すべてに公開のsetter。 残高や状態に
{ get; set; }を付けると、不正な状態を招きます。private set、init、読み取り専用を使い、状態はメソッドを通して変更します。 - 読み取り専用のプロパティから変更可能なコレクションを返す。
public List<string> Tags { get; }は、呼び出し側がリストを置き換えるのは防ぎますが、追加は防ぎません。外から変更されるべきでないコレクションにはIReadOnlyList<string>を返します。
よくある質問
C#の{ get; set; }とはどういう意味ですか?
自動実装プロパティを宣言します。コンパイラが、隠れたprivateなフィールドと、それを返す get アクセサー、それに代入する set アクセサーを作ります。public string Name { get; set; } は呼び出し側からは公開フィールドのように振る舞いますが、実体はメソッドの組なので、後から検証を加えたり private set に変えたりしても、それを使うコードを変える必要はありません。
C#のフィールドとプロパティの違いは何ですか?
フィールドはオブジェクトに格納される変数です。プロパティは、外からはフィールドのように見えるメソッドの組(get と set)です。プロパティを使うと、クラスは値を検証したり、結果を計算したり、書き込みを制限したりでき、データバインディング、シリアライザー、インターフェースが扱うのもプロパティです。公開するデータはプロパティとして公開し、フィールドは通常privateにしておきます。
C#のprivate setとは何ですか?
public int Stock { get; private set; } は、どのコードからもプロパティを読めますが、変更できるのはクラスの中のコードだけにします。残高や数などクラスが所有する状態を公開しつつ、すべての変更をルールを守らせるメソッドの後ろに置く方法です。
C#のinitは何をしますか?
init(C# 9)は、オブジェクトの作成中、つまりコンストラクターかオブジェクト初期化子の中でだけ使えるsetterです。public string Sku { get; init; } は new Product { Sku = "A-1" } を許しますが、その後の product.Sku = "B-2" はエラーCS8852で拒否します。不変のオブジェクトに初期化子の構文を与えます。
C#のrequiredプロパティとは何ですか?
required(C# 11)を付けたプロパティは、その型を作るすべてのオブジェクト初期化子で設定しなければならず、省略するとコンパイルエラー(CS9035)になります。init とよく組み合わせます。public required string Email { get; init; } は作成時に必ず与えなければならず、後から変更できません。