C# 14: field キーワードと field によって裏付けられたプロパティ
C# 14 はプロパティのアクセサー向けに文脈依存キーワード field を導入し、別途バッキングフィールドを宣言せずに自動プロパティへカスタムロジックを追加できるようにします。
C# 14 は新しい文脈依存キーワード field を導入します。これはプロパティのアクセサー (get、set、init ブロック) の中で、プロパティのバッキング ストレージを指すために使えます。簡単に言えば、field はプロパティの値が格納される隠れた変数を表すプレースホルダーです。このキーワードを使うと、別の private なフィールドを手で宣言することなく、自動実装プロパティにカスタムロジックを加えることができます。最初は C# 13 のプレビューで使えるようになり (.NET 9 で言語バージョンを preview に設定する必要がありました)、C# 14 で正式に言語の一部になりました。
なぜ便利なのか? C# 14 より前は、プロパティにロジック (バリデーションや変更通知など) を加えたい場合、フルプロパティに変えて private なバッキングフィールドを用意する必要がありました。これは定型コードを増やし、他のクラスメンバーがその private フィールドに直接アクセスしてプロパティのロジックを回避するリスクももたらしました。新しい field キーワードは、コンパイラーがバッキングフィールドの生成と管理を引き受ける一方で、あなたはプロパティのコードで field を使うだけ、という形でこれらの問題を解消します。結果として、より明快でメンテナンス性の高いプロパティ宣言になり、バッキング ストレージがクラスの他のスコープへ “漏れ出す” こともなくなります。
field のメリットとユースケース
field キーワードは、プロパティ宣言をより簡潔でエラーが起きにくいものにするために導入されました。主なメリットと有用なシナリオは次のとおりです。
-
手書きのバッキングフィールドの削除: カスタム動作を加えるためだけに、プロパティごとに private なメンバーフィールドを書く必要はもうありません。コンパイラーが隠しバッキングフィールドを自動的に提供し、
fieldキーワード経由でアクセスします。これにより定型コードが減り、クラス定義がよりすっきりします。 -
プロパティの状態をカプセル化: コンパイラーが作成するバッキングフィールドは、プロパティのアクセサー経由 (
field経由) でしかアクセスできず、クラス内の他の場所からは触れません。これは他のメソッドやプロパティからのフィールドへの不用意なアクセスを防ぎ、プロパティ アクセサー内の不変条件やバリデーションが回避されないようにします。 -
より簡単なプロパティロジック (バリデーション、遅延初期化など): 自動プロパティにロジックを加える滑らかな道筋を提供します。よくあるシナリオには次のようなものがあります。
- バリデーションや範囲チェック: たとえば、値を受け入れる前に非負であるか、ある範囲内であるかを確認する。
- 変更通知: たとえば、新しい値を設定した後に
INotifyPropertyChangedイベントを発火する。 - 遅延初期化やデフォルト化: たとえば、ゲッターで最初のアクセス時に
fieldを初期化する、あるいは未設定ならデフォルトを返す。
以前の C# では、これらのシナリオには別のフィールドを伴うフルプロパティを書く必要がありました。
fieldを使えば、これらをプロパティのget/setロジックの中で、追加フィールドなしに直接実装できます。 -
自動アクセサーとカスタムアクセサーの混在: C# 14 では、片方のアクセサーを自動実装にして、もう片方を
fieldを使った本体つきにできます。たとえば、カスタムのsetを提供し、getを自動のままにすることも、その逆もできます。コンパイラーは書かれていないアクセサーに必要なものを生成します。これは以前は不可能でした。以前は片方のアクセサーに本体を追加すると、両方とも明示的に実装する必要がありました。
全体として、field は冗長なコードを取り除き、必要なカスタム動作だけに集中することで、可読性とメンテナンス性を高めます。概念的には、setter での value キーワード (代入される値を表す) の動きに似ています。ここでは field がプロパティの内部ストレージを表します。
比較: 手書きのバッキングフィールド vs. field キーワード
違いを見るために、何らかのルールを強制するプロパティを C# 14 以前 と 以後 (新しい field キーワードを使った場合) でどう宣言するか比較してみましょう。
シナリオ: プロパティ Hours を、決して負の数に設定できないようにしたいとします。古い C# では次のようにしていました。
C# 14 以前: 手書きのバッキングフィールドを使う場合:
public class TimePeriodBefore
{
private double _hours; // backing field
public double Hours
{
get { return _hours; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
_hours = value;
}
}
}
この C# 14 以前のコードでは、値を保持するために private フィールド _hours を導入する必要がありました。プロパティのゲッターはこのフィールドを返し、セッターは _hours への代入前にチェックを行います。これは動作しますが冗長です。_hours を宣言・管理するための余分なコードがあり、_hours はクラス内のどこからでもアクセスできます (つまり、他のメソッドが不注意に _hours に書き込んで、バリデーションロジックを回避する 可能性 があります)。
C# 14 以降: field キーワードを使う場合:
public class TimePeriod
{
public double Hours
{
get; // auto-implemented getter (compiler provides it)
set => field = (value >= 0)
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
}
}
ここでは、プロパティ Hours は明示的なバッキングフィールドなしで宣言されています。get; を本体なしで使い、自動ゲッターを示し、set には field を使う本体を与えています。セッター内の式 field = ... は、コンパイラーに「プロパティのバッキングフィールドへ代入せよ」と伝えます。コンパイラーは舞台裏で自動的に private フィールドを生成し、get アクセサーがそのフィールドを返すように実装します。上の setter では、value が負なら例外を投げ、そうでなければ field (それを保持する) に代入します。_hours を 自分で 宣言する必要はなく、ゲッターの本体を書く必要もありません。コンパイラーがそれらをやってくれます。結果として、同じ動作を維持しつつ、より簡潔なプロパティ定義になります。
C# 14 のバージョンがどれほどすっきりしているかに注目してください。
- 明示的な
_hoursフィールドを取り除きました。コンパイラーがそれを処理します。 getアクセサーはシンプルな自動実装 (get;) のままで、コンパイラーが「バッキングフィールドを返す」という形に変えてくれます。setアクセサーには気になるロジック (非負チェック) だけが含まれ、実際のストレージへの代入はfield = valueが処理します。
必要であれば、get アクセサーでも field を使えます。たとえば、遅延初期化を実装するなら次のように書けます。
public string Name
{
get => field ??= "Unknown";
set => field = value;
}
このケースでは、最初に Name にアクセスしたとき、まだ設定されていなければ、ゲッターはバッキングフィールドにデフォルト "Unknown" を割り当てて返します。以降の get や set は同じ field を使います。この機能がなければ、同じ動作を実現するために private フィールドとゲッター内の追加コードが必要でした。
コンパイラーは field キーワードをどう扱うのか?
プロパティ アクセサー内で field を使うと、コンパイラーはそのプロパティの隠しバッキングフィールドを静かに生成します (自動実装プロパティに対する処理に非常に似ています)。このフィールドはソースコードでは決して見えませんが、コンパイラーが内部名 (たとえば <Hours>k__BackingField のようなもの) を付け、プロパティの値を保持するために使います。内部では次のようなことが起きています。
- バッキングフィールドの生成: プロパティの少なくとも 1 つのアクセサーが
fieldを使っているとき (あるいは本体のない自動実装プロパティの場合)、コンパイラーは値を保持するための private フィールドを作成します。このフィールドを自分で宣言する必要はありません。上のTimePeriod.Hoursの例では、コンパイラーが時間値を保持するフィールドを生成し、getとsetの両アクセサーがそのフィールド上で (暗黙的またはfieldキーワード経由で) 動作します。 - ゲッター/セッターの実装:
- 自動実装アクセサー (本体のない
get;やset;など) では、コンパイラーがバッキングフィールドを返す/設定するシンプルなロジックを自動的に生成します。 fieldを使った本体を提供したアクセサーでは、コンパイラーがあなたのロジックをインライン化し、生成コード内のfieldをバッキングフィールドへの参照として扱います。たとえばset => field = value;はコンパイル後の出力ではset { backingField = value; }のようなものになり、あなたが書いた追加のロジックがその周りに保たれます。- 自動アクセサーとカスタム アクセサーは混在させられます。たとえば、
setの本体を書き (fieldを使って)、getをget;のままにすると、コンパイラーがgetを生成してくれます。逆に、カスタムのget(例:get => ComputeSomething(field)) を書き、set;を自動実装のままにすると、コンパイラーは単にバッキングフィールドへ代入するセッターを生成します。
- 自動実装アクセサー (本体のない
- 手書きフィールドと等価な振る舞い:
fieldを使ったコンパイル結果は、手書きで private フィールドを書いてプロパティで使った場合と本質的に同じです。パフォーマンス上のペナルティはなく、定型コードを書かずに済む以上の魔法もありません。これは純粋にコンパイル時の利便機能です。たとえば上のHoursの 2 つの実装 (fieldあり/なし) は非常に似た IL にコンパイルされます。どちらも値を保持する private フィールドと、そのフィールドを操作するプロパティ アクセサーを持ちます。違いは、C# 14 のコンパイラーが片方のためのコードを書いてくれている点です。 - プロパティ初期化子:
fieldを使うプロパティに初期化子を使うと (例:public int X { get; set => field = value; } = 42;)、初期化子は伝統的な自動プロパティと同様に、コンストラクターが走る 前 にバッキングフィールドを直接初期化します。オブジェクト構築中に setter のロジックは 呼ばれません。(setter に副作用がある場合、初期化子経由の初期値設定では発火しないので注意してください。初期化時にも setter のロジックを動かしたい場合は、初期化子ではなくコンストラクターでプロパティを代入してください。) - バッキングフィールドへの属性付与: 生成されたバッキングフィールドに属性を付けたい場合、C# は
[field: ...]構文による フィールド対象の属性 をサポートします。これは自動プロパティで既に可能でしたが、ここでも機能します。たとえば[field: NonSerialized] public int Id { get; set => field = value; }と書いて、自動生成されたフィールドを非シリアル化対象としてマークできます。(これはプロパティに実際にバッキングフィールドが存在する場合 (つまり、少なくとも 1 つのアクセサーがfieldを使っているか、自動プロパティである場合) のみ機能します。)
要するに、コンパイラーが private なバッキングフィールドを生成し、プロパティ アクセサーがそれを使うように配線してくれます。あなたは少ないコードでフルプロパティの機能を得られます。実装上、プロパティは依然として真の自動プロパティのままで、ロジックを差し込むためのフックを得たというだけです。
field の構文と使用ルール
field キーワードを使うときは、次のルールと制約を念頭に置いてください。
- プロパティ/インデクサーのアクセサー内でのみ:
fieldはプロパティまたはインデクサーのアクセサーの本体 (get、set、initのコードブロックや式) 内で のみ 使えます。これは 文脈依存 のキーワードであり、プロパティのアクセサー外ではfieldに特別な意味はありません (単なる識別子と見なされます)。通常のメソッドやプロパティ外でfieldを使おうとすると、コンパイル エラーになります。コンパイラーがどのバッキングフィールドを指しているのか分からないからです。 - 文脈依存キーワード (完全には予約されていない):
fieldはグローバルに予約されたキーワードではないため、技術的にはコードの他の部分にfieldという名前の変数やメンバーがあってもかまいません。ただし、プロパティのアクセサー内ではfieldはキーワードとして扱われ、fieldという名前の変数ではなくバッキングフィールドを参照します。下の “命名の衝突” 節も参照してください。 - get/set/init アクセサーでの利用:
fieldはget、set、initアクセサー内で使えます。setter や init アクセサーでは通常fieldに代入します (例:field = value;)。getter ではfieldを返すか変更します (例:return field;やfield ??= defaultValue;)。必要に応じて、片方のアクセサーで使うことも、両方で使うこともできます。fieldを 片方のアクセサーだけ で使う場合、もう片方は自動実装 (get;または本体のないset;) のままで構いません。コンパイラーが依然としてバッキングフィールドを生成し、すべてを配線してくれます。fieldを 両方 のアクセサーで使うのも問題ありません。事実上、get と set のロジックを書いていることになります (それでもフィールドを手で宣言する必要はありません)。読み込みと書き込みの両方に特別な処理が必要なときに有効です。たとえば、setter は条件を強制し、getter は最初のアクセス時に変換や遅延読み込みを行うといったケースで、いずれも同じfieldを利用できます。
- アクセサー外で
fieldを参照できない:fieldの参照を保存して別の場所で使うことはできず、コンパイラー生成のバッキングフィールドにプロパティの外から直接アクセスすることもできません。事実上、ソースコード上ではバッキングフィールドは無名です (内部的にはコンパイラーが名前を付けますが)。値とやり取りしたい場合は、プロパティ経由か、そのアクセサー内でfieldを使ってください。 - イベントには使えない:
fieldキーワードはプロパティ (とインデクサー) を対象としています。イベントの add/remove アクセサーでは利用 できません。(C# のイベントもデリゲート用のバッキングフィールドを持つことがありますが、言語チームはfieldをイベント アクセサーへ拡張しないことを決めました。) - 明示的なフィールド宣言と混在させない: プロパティのために自分のバッキングフィールドを宣言する場合、そのプロパティのアクセサーで
fieldを使うべきではありません。その場合は、従来どおり明示的なフィールドを名前で参照します。fieldキーワードはそうしたシナリオで明示フィールドを置き換えるためのものです。つまりプロパティは、コンパイラー管理の暗黙フィールド (fieldまたは自動アクセサーを使う場合) を持つか、自分で管理するかのどちらかであり、両方ではありません。
簡単に言えば、プロパティのアクセサーの中ではそのプロパティの隠しストレージを参照するために field を使い、それ以外では使わないでください。プロパティの外側のものについては、通常の C# のスコープ規則に従います。
命名の衝突への対処 (field という独自の変数を持っている場合)
以前の C# では field は予約語ではなかったため、(まれですが) 一部のコードが “field” を変数名やフィールド名として使っていた可能性があります。アクセサーに文脈依存キーワード field が導入されたことで、そうしたコードはあいまいになったり壊れたりする可能性があります。言語設計はこれを考慮しています。
- アクセサー内の
fieldは識別子をシャドウする: プロパティ アクセサー内では、新しいキーワードfieldがそのスコープ内にあるfieldという名前の任意の識別子を シャドウ します。たとえば setter 内に (古いコード由来の)fieldという名前のローカル変数や引数があれば、コンパイラーはfieldをあなたの変数ではなくバッキングフィールドのキーワードとして解釈するようになりました。C# 14 では、アクセサー内でfieldという名前の変数を宣言・使用しようとするとコンパイル エラーになります。fieldは今やキーワードであることが期待されているからです。 @fieldまたはthis.fieldを使って実際のフィールドを参照: クラスに文字どおり “field” という名のメンバーフィールドが 実在する 場合 (推奨されませんが可能です)、あるいはスコープ内に “field” という変数があれば、名前をエスケープすることで参照できます。C# では識別子の前に@を付けることで、それがキーワードであっても識別子として使うことができます。たとえば、クラスにprivate int field;があり、それをアクセサーで参照したい場合は@fieldと書けば識別子としてアクセスできます。同様に、this.fieldを使えばメンバー フィールドを明示的に参照できます。@や修飾子を使うことで文脈依存キーワードとしての解釈を回避し、実際の変数にアクセスできます。
private int field = 10; // a field unfortunately named "field"
public int Example
{
get { return @field; } // use @field to return the actual field
set { @field = value; } // or this.field = value; either works
}
- ただし、可能であればメンバーを名前変更して混乱を避けるほうが良いでしょう。モダンな C# では、アクセサーで単独の
fieldはコンパイラーのバッキングフィールド用に予約されているとみなすべきです。実際、古いコードベースを C# 14 にアップグレードすると、以前は別のものを参照していたfieldの使用がコンパイラーから警告され、曖昧さを解消するよう促されます。 - そもそも名前を避ける: 一般的なベストプラクティスとして、コードで
fieldを識別子名として使わないようにしてください。今や (文脈で) キーワードである以上、通常の名前として扱うと読み手を混乱させ、エラーにつながり得ます。fieldを変数名として使っていたなら、C# 14 へ移行する際にリネームを検討してください。よくある命名規約 (private フィールドに_を付けるなど) を採れば、ほとんどの場合この衝突は自然に避けられます。
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.