次の方法で共有


fieldプロパティ内のキーワード

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

チャンピオン号: https://github.com/dotnet/csharplang/issues/8635

概要

新しいコンテキスト キーワード field を使用して、自動的に生成されたバッキング フィールドを参照できるように、すべてのプロパティを拡張します。 プロパティには、ボディなしのアクセサーの他に、ボディ付きのアクセサーを含めることもできます。

モチベーション

自動プロパティでは、バッキング フィールドを直接設定または取得することのみが可能で、アクセサーにアクセス修飾子を配置することによってのみ制御できます。 一方または両方のアクセサーでの挙動を追加で制御する必要がある場合がありますが、このとき、ユーザーはバッキングフィールドを宣言する手間に直面することになります。 その後、バッキング フィールド名はプロパティと同期する必要があり、バッキング フィールドのスコープはクラス全体に設定されるため、クラス内からアクセサーが誤ってバイパスされる可能性があります。

いくつかの一般的なシナリオがあります。 ゲッター内には、プロパティが指定されていない場合は遅延初期化または既定値があります。 セッター内には、値の有効性を確保するための制約を適用するか、INotifyPropertyChanged.PropertyChanged イベントを発生させるなどの更新を検出して伝達します。

このような場合は、常にインスタンス フィールドを作成し、プロパティ全体を自分で記述する必要があります。 これにより、かなりの量のコードが追加されるだけでなく、アクセサーの本体でのみ使用できるようにすることが望ましい場合であっても、バッキング フィールドが型の範囲の残りの部分に漏れてしまいます。

用語集

  • 自動プロパティ: "自動的に実装されるプロパティ" の省略形 (§15.7.4)。 自動プロパティのアクセサーには本文がありません。 実装とバッキング ストレージはどちらもコンパイラによって提供されます。 自動プロパティには、{ get; }{ get; set; }、または { get; init; } があります。

  • オート アクセサー: "自動的に実装されるアクセサー" の省略形。これは、本文のないアクセサーです。 実装とバッキング ストレージはどちらもコンパイラによって提供されます。 get;set;、および init; は自動アクセサーです。

  • フル アクセサー: 本文を持つアクセサーです。 実装はコンパイラによって提供されませんが、バッキング ストレージは引き続き (例 set => field = value; のように) 存在する可能性があります。

  • フィールドに基づくプロパティ: アクセサー本体内で field キーワードを使用するプロパティ、または自動プロパティです。

  • バッキング フィールド: これは、プロパティのアクセサーの field キーワードによって示される変数です。これは、自動的に実装されるアクセサー (get;set;、または init;) でも暗黙的に読み取りまたは書き込まれます。

詳細な設計

init アクセサーを持つプロパティの場合、set に以下で適用されるすべてのものが、代わりに init アクセサーに適用されます。

構文には次の 2 つの変更があります。

  1. 新しいコンテキストキーワード fieldが導入されました。このキーワードは、プロパティアクセサーの本体内で用いられ、プロパティ宣言のバッキングフィールドにアクセスできます (LDM の決定)。

  2. プロパティは、自動アクセサーとフルアクセサーを組み合わせて使用できるようになりました(LDM 決定)。 「自動プロパティ」は、アクセサーに本文がないプロパティを指します。 以下の例はいずれも自動プロパティと見なされることはありません。

例:

{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }

両方のアクセサーは、fieldを利用して、いずれかまたは両方が完全なアクセサーとして機能する場合があります。

{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
    get;
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged();
    }
}

式形式のプロパティと、getアクセサーfieldのみを持つプロパティでも使用できます。

public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }

set-only プロパティでは、fieldを使用することもあります。

{
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged(new XyzEventArgs(value));
    }
}

重大な変更

プロパティ アクセサー本体内に field コンテキスト キーワードが存在することは、破壊的変更になる可能性があります。

field はキーワードであり、識別子ではないため、通常のキーワードエスケープ ルート (@field) を使用して識別子によってのみ "シャドウ" できます。 プロパティ アクセサー本体内で宣言された field という名前のすべての識別子は、最初の @ を追加することで、14 より前の C# バージョンからアップグレードするときに中断から保護できます。

プロパティ アクセサーで field という名前の変数が宣言されている場合は、エラーが報告されます。

言語バージョン 14 以降では、 プライマリ式field がバッキング フィールドを参照しているが、以前の言語バージョンでは別のシンボルを参照していた場合、警告が報告されます。

フィールドに特化した属性

自動プロパティと同様に、アクセサーの 1 つでバッキング フィールドを使用するプロパティでもフィールド ターゲット属性を使用できます。

[field: Xyz]
public string Name => field ??= Compute();

[field: Xyz]
public string Name { get => field; set => field = value; }

アクセサーがバッキング フィールドを使用しない限り、フィールド ターゲット属性は無効なままです。

// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();

プロパティの初期化子

初期化子を持つプロパティでは field を使用できます。 バッキングフィールドはセッターが呼び出されるのではなく、直接初期化されます (LDM 決定)。

初期化子のセッターの呼び出しはオプションではありません。初期化子は基本コンストラクターを呼び出す前に処理され、基本コンストラクターが呼び出される前にインスタンス メソッドを呼び出すことは不正となります。 これは、構造体の既定の初期化/確定割り当てにも重要です。

これにより、初期化を柔軟に制御できます。 セッターを呼び出さずに初期化する場合はプロパティ初期化子を使用します。 セッターを呼び出して初期化する場合は、コンストラクターでプロパティに初期値を割り当てます。

これが役に立つ例を次に示します。 field キーワードは、INotifyPropertyChanged パターンに優れたソリューションがあるため、ビュー モデルで多くの用途が見つかると考えています。 ビュー モデル のプロパティ セッターは、UI にデータバインドされる可能性があり、変更の追跡や他の動作のトリガーを引き起こす可能性があります。 次のコードでは、IsActiveHasPendingChanges に設定せずに、true の既定値を初期化する必要があります。

class SomeViewModel
{
    public bool HasPendingChanges { get; private set; }

    public bool IsActive { get; set => Set(ref field, value); } = true;

    private bool Set<T>(ref T location, T value)
    {
        if (EqualityComparer<T>.Default.Equals(location, value))
            return false;

        location = value;
        HasPendingChanges = true;
        return true;
    }
}

プロパティ初期化子とコンストラクターからの割り当ての動作のこの違いは、以前のバージョンの言語の仮想自動プロパティでも確認できます。

using System;

// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();

class Base
{
    public virtual bool IsActive { get; set; } = true;
}

class Derived : Base
{
    public override bool IsActive
    {
        get => base.IsActive;
        set
        {
            base.IsActive = value;
            Console.WriteLine("This will not be reached");
        }
    }
}

コンストラクターの割り当て

自動プロパティと同様に、コンストラクターの割り当ては (仮想的な可能性がある) セッターが存在する場合にセッターを呼び出し、セッターがない場合はバッキング フィールドに直接割り当てることにフォールバックします。

class C
{
    public C()
    {
        P1 = 1; // Assigns P1's backing field directly
        P2 = 2; // Assigns P2's backing field directly
        P3 = 3; // Calls P3's setter
        P4 = 4; // Calls P4's setter
    }

    public int P1 => field;
    public int P2 { get => field; }
    public int P4 { get => field; set => field = value; }
    public int P3 { get => field; set; }
}

構造体での明確な割り当て

コンストラクターで参照することはできませんが、 field キーワードで示されるバッキング フィールドは、他の構造体フィールドと同じ条件 (LDM 決定 1LDM 決定 2) の下で、既定の初期化と既定で無効にされる警告の対象となります。

次に例を示します (これらの診断は既定ではサイレントです)。

public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        _ = P1;
    }

    public int P1 { get => field; }
}
public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        P2 = 5;
    }

    public int P2 { get => field; set => field = value; }
}

参照を返すプロパティ

自動プロパティと同様に、field キーワードは参照を返すプロパティでは使用できません。 参照を返すプロパティは、set アクセサーを持つことはできません。set アクセサーがないと、get アクセサーとプロパティ初期化子だけがバッキング フィールドにアクセスできます。 このためのユース ケースはありません。現在は、参照を返すプロパティを自動プロパティとして書き込めるタイミングではありません。

NULL 値の許容

Null 許容参照型機能の原則は、C# の既存の慣用的なコーディング パターンを理解し、それらのパターンに関する形式的な操作をできるだけ少なくすることです。 field キーワードの提案により、単純で慣用的なパターンが可能になり、遅延初期化プロパティなど、広く求められているシナリオに対処できます。 Null 許容参照型は、これらの新しいコーディング パターンとうまく統合されることが重要です。

目標:

  • field キーワード機能のさまざまな使用パターンに対して、適切なレベルの null 安全性を確保する必要があります。

  • field キーワードを使用するパターンは、常に言語の一部であったかのように感じる必要があります。 field キーワード機能に完全に慣用的なコードで Null 許容参照型を有効にするために、ユーザーが面倒な手順を踏まないように注意してください。

主なシナリオの 1 つは、遅延初期化プロパティです。

public class C
{
    public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here

    string Prop => field ??= GetPropValue();
}

次の null 許容ルールは、field キーワードを使用するプロパティだけでなく、既存の自動プロパティにも適用されます。

バッキング フィールドの NULL 値の許容

新しい用語の定義については、 用語集 を参照してください。

バッキング フィールドの型はプロパティと同じです。 ただし、許容注釈はプロパティと異なる場合があります。 この NULL 許容注釈を決定するために、NULL 回復性の概念を紹介します。 Null 回復性は、直感的に、フィールドに型のget値が含まれている場合でも、プロパティの default アクセサーが null 安全性を保持することを意味します。

フィールド バック プロパティは、そのアクセサの特別な NULL 許容分析を実行することによって、NULL 回復性があるかどうかがget決定されます。 この分析は、プロパティが参照型であり、注釈が付かない場合にのみ実行されることに注意してください。

  • 2 つの個別の null 許容分析パスが実行されます。1 つは、 fieldの null 許容注釈に注釈が 付けず、もう 1 つは null 許容注釈に注釈が 付けられます。 各分析に起因する null 許容診断が記録されます。
  • 注釈付きパスに null 許容診断があり、注釈のないパスに存在しなかった場合、プロパティは null 回復性がありません。 それ以外の場合は、null 回復性があります。
    • 実装では、最初に 注釈付き パスを実行することで最適化できます。 結果として診断がまったく行われなかった場合、プロパティは null 回復性があり、 注釈のない パスはスキップできます。
  • プロパティに get アクセサーがない場合、そのプロパティは (空虚に) NULL 回復性を持ちます。
  • get アクセサーが自動実装されている場合、プロパティには null 回復性がありません。

null 許容 (!) 演算子、 #nullable disable#pragma disable warningなどのディレクティブ、および従来のプロジェクト レベルの設定 ( <NoWarn>など) はすべて、null 許容分析に診断があるかどうかを判断するときに考慮されます。 Null 許容分析に診断があるかどうかを判断する場合、DiagnosticSuppressor は無視されます。

バッキング フィールドの null 許容注釈は、次のように決定されます。

  • 関連付けられたプロパティの null 許容注釈に注釈が 付けられている、または認識できない場合、null 許容注釈は、関連付けられたプロパティの null 許容注釈と同じです。
  • 関連付けられているプロパティの null 許容注釈に 注釈が付けられていない場合は、次のようになります。
    • プロパティが null 回復性を持つ場合、null 許容注釈に 注釈が付けられます
    • プロパティが null 回復性がない場合、null 許容注釈は 注釈付けされません

コンストラクターの分析

現在、自動プロパティは null 許容コンストラクター分析の通常のフィールドとよく似た方法で扱われます。 この処理は、すべてのフィールドに基づくプロパティ をそのバッキングフィールドへのプロキシとして扱うことで、フィールドに基づくプロパティ まで拡張されます。

これを実現するために、前に 提案したアプローチ から次の仕様言語を更新します ( 太字の新しい言語)。

コンストラクター内の明示的または暗黙的な「戻り値」ごとに、フロー状態が注釈および null 許容属性と互換性のない各メンバーに対して警告を表示します。 メンバーがフィールドに基づくプロパティの場合、バッキング フィールドの NULL 許容注釈がこのチェックに使用されます。 それ以外の場合は、メンバー自体の NULL 許容注釈が使用されます。 これに対する適切なプロキシは、戻りポイントでメンバーを自身に割り当てると NULL 値の許容の警告が生成された場合、戻りポイントで NULL 値の許容の警告が生成されます。

null 回復性を持つプロパティの主な特徴は、バッキング フィールドの値がdefaultされている場合でも、"正しい" null 許容を持つ値を返すということです。 これを考えると、このようなプロパティのフロー状態を追跡することも検討できませんでした。 しかし、これはプロパティの null 許容分析の大きな変化のように思えます。現時点では、変更の動機付けシナリオはありません。

これは、本質的に制約付き相互解析です。 コンストラクターを分析するには、 field コンテキスト キーワードを使用し、 注釈のない null 値の許容を持つ、同じ型で適用可能なすべての get アクセサーに対してバインディングと "null 回復性" 分析を行う必要があると予測しています。 ゲッター本体は通常あまり複雑ではなく、型内のコンストラクターの数に関係なく、「null 回復性」分析を 1 回だけ実行する必要があるため、これが法外に高価になることはないと推測しています。

セッター分析

わかりやすくするために、「setter」および「set アクセサー」という用語を使って、set アクセサーまたは init アクセサーを指します。

null 非回復性のフィールドに基づくプロパティのセッターが実際にバッキング フィールドを初期化することを確認する必要があります。

class C
{
    string Prop
    {
        get => field;

        // getter is not null-resilient, so `field` is not-annotated.
        // We should warn here that `field` may be null when exiting.
        set { }
    }

    public C()
    {
        Prop = "a"; // ok
    }

    public static void Main()
    {
        new C().Prop.ToString(); // no warning, NRE at runtime
    }
}

フィールドバックプロパティのセッター内のバッキングフィールドの初期フロー状態は、次のように決定されます。

  • プロパティに初期化子がある場合、初期フロー状態は初期化子にアクセスした後のフロー状態と同じです。
  • それ以外の場合、初期フロー状態は field = default; によって与えられるフロー状態と同じです。

セッターの明示的または暗黙的な各リターンで、バッキングフィールドのフロー状態が、その注釈およびnull許容属性と互換性がない場合、警告が報告されます。

注釈

この定式化は、コンストラクターの通常のフィールドと意図的に似ています。 基本的に、プロパティ アクセサーのみが実際にバッキング フィールドを参照できるため、セッターはバッキング フィールドの「ミニ コンストラクター」として扱われます。

通常のフィールドと同様に、プロパティが設定されたためにコンストラクターで初期化されたことは通常わかっていますが、必ずしもそうとは限りません。 Prop != null が true であったブランチ内に戻るだけで、コンストラクターの分析にも十分です。これは、追跡されていないメカニズムがプロパティの設定に使用されている可能性があることを理解しているためです。

代替案が検討されました。 「Nullability alternatives 」セクションを参照してください。

nameof

field がキーワードである場所では、nameof(field) のように、 はコンパイルに失敗します (nameof(nint))。 nameof(value) とは違います。これは、プロパティ セッターが .NET Core ライブラリの場合と同様に ArgumentException をスローするときに使用します。 これに対し、nameof(field) には想定されるユース ケースはありません。

オーバーライド

プロパティをオーバーライドする場合、fieldを使用できます。 このような field の使用法は、オーバーライドするプロパティのためのバッキング フィールドを参照しています。これは、ベース プロパティのバッキングフィールドがある場合、別のバッキング フィールドです。 これによりカプセル化が中断されるため、基本プロパティのバッキング フィールドをオーバーライドするクラスに公開するための ABI はありません。

自動プロパティと同様に、 field キーワードを使用し、基本プロパティをオーバーライドするプロパティは、すべてのアクセサーをオーバーライドする必要があります (LDM 決定)。

キャプチャ

field はローカル関数とラムダでキャプチャでき、他の参照がない場合でも、ローカル関数とラムダ内からの field への参照が許可されます (LDM 決定 1LDM 決定 2)。

public class C
{
    public static int P
    {
        get
        {
            Func<int> f = static () => field;
            return f();
        }
    }
}

フィールドの使用に関する警告

アクセサーで field キーワードを使用すると、割り当てられていないフィールドまたは未読フィールドのコンパイラの既存の分析にそのフィールドが含まれます。

  • CS0414: プロパティ 'Xyz' のバッキング フィールドが割り当てられていますが、その値が使用されることはありません
  • CS0649: プロパティ 'Xyz' のバッキング フィールドが割り当てられることはなく、常に既定値になります

仕様の変更

構文

言語バージョン 14 以上でコンパイルする場合、次の場所 (field 決定) でプライマリ式 (LDM 決定) として使用する場合、はキーワードと見なされます。

  • プロパティ内の getset、および init アクセサのメソッド本体内 (インデクサ内ではない)
  • これらのアクセサーに適用される属性
  • 入れ子になったラムダ式とローカル関数、およびそれらのアクセサー内での LINQ 式

言語バージョン 12 以下でコンパイルする場合を含め、他のすべてのケースでは、field は識別子と見なされます。

primary_no_array_creation_expression
    : literal
+   | 'field'
    | interpolated_string_expression
    | ...
    ;

プロパティ

§15.7.1プロパティ - 全般

property_initializer は、自動的に実装されるプロパティと、出力されるバッキング フィールドを持つプロパティに対してのみ指定できます。property_initializer は、このようなプロパティの基になるフィールドを、で指定された値で初期化します。

§15.7.4プロパティを自動的に実装する

自動的に実装されたプロパティ (略して自動プロパティ) は、セミコロンのみのアクセサー本体を持つ、非抽象、非 extern、非参照値のプロパティです。自動プロパティには get アクセサーが必要で、オプションで set アクセサーを持つこともできます。次のいずれかまたは両方:

  1. セミコロンのみの本体を持つアクセサー
  2. アクセサー内またはfieldプロパティの式本文内でのコンテキスト キーワードの使用

プロパティが自動的に実装されるプロパティとして指定されている場合、非表示の 名前のない バッキング フィールドがプロパティに対して自動的に使用でき 、アクセサーはそのバッキング フィールドの読み取りと書き込みを行うために実装されます自動プロパティの場合、セミコロンのみのgetアクセサーは読み取り用に実装され、セミコロンのみのsetアクセサーはバッキングフィールドへの書き込み用に実装されます。

隠しバッキング フィールドにアクセスできない場合は、自動的に実装されるプロパティ アクセサーを介してのみ読み取りおよび書き込みを行うことができます(包含型内でも)。バッキング フィールドは、すべてのアクセサー内およびプロパティ式本文内で、field キーワードを使用して直接参照できます。フィールドは名前が付いていないため、nameof 式では使用できません。

自動プロパティに< c0 >set アクセサーがなく、< c1 >get アクセサーがセミコロンのみの場合、バッキング フィールドは< c2 />と見なされます (< c3 >§15.5.3< /c3 >)。 readonly フィールドと同様に、読み取り専用のオートプロパティ (set アクセサーまたは init アクセサーなし) は、包囲クラスのコンストラクターの本体で割り当てることも可能です。 このような割り当ては、プロパティの 読み取り専用 バッキング フィールドに直接割り当てられます。

自動プロパティは、set アクセサーがない状態で、セミコロンのみの get アクセサーを持つことはできません。

自動プロパティには必要に応じて property_initializerがあり、バッキング フィールドに variable_initializer として直接適用されます (§17.7)。

次のような例です。

// No 'field' symbol in scope.
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

これは次の宣言と同等です。

// No 'field' symbol in scope.
public class Point
{
    public int X { get { return field; } set { field = value; } }
    public int Y { get { return field; } set { field = value; } }
}

これは次に相当します。

// No 'field' symbol in scope.
public class Point
{
    private int __x;
    private int __y;
    public int X { get { return __x; } set { __x = value; } }
    public int Y { get { return __y; } set { __y = value; } }
}

次のような例です。

// No 'field' symbol in scope.
public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

これは次の宣言と同等です。

// No 'field' symbol in scope.
public class Point
{
    private string __value;
    public string Value { get { return __value ??= ComputeValue(); } }
    private static string ComputeValue() { /*...*/ }
}

選択肢

NULL 許容の代替方法

Nullability」セクションで概説されている null 回復性アプローチに加えて、作業グループは LDM の考慮事項に対して次の代替手段を提案しました。

何もしない

ここでは特別な動作をまったく導入できませんでした。 事実上、次のようになります。

  • フィールドベースのプロパティは、現在の自動プロパティと同じように扱います。ただし、必須としてマークされている場合を除いて、コンストラクターで初期化する必要があります。
  • プロパティ アクセサーを分析する場合、フィールド変数の特別な扱いはありません。 これは単に、プロパティと同じ型と null 許容を持つ変数です。

これにより、「遅延プロパティ」シナリオで迷惑警告が発生します。その場合、ユーザーはコンストラクターの警告を無効化するために、null! またはそれに類似したものを割り当てる必要があるでしょう。
考慮できる代替方法の 1 つは、NULL 許容コンストラクター分析に field キーワードを使用してプロパティを完全に無視することです。 その場合、使用している初期化パターンに関係なく、ユーザーが何かを初期化する必要があるという警告はなく、ユーザーにとって迷惑になることもありません。

.NET 9 のプレビュー LangVersion の下で field キーワード機能を出荷する予定であるため、.NET 10 ではその機能の null 許容動作を変更できる可能性があると考えています。 したがって、このような"低コスト"ソリューションを短期的に採用し、長期的にはより複雑なソリューションの 1 つに成長させることを検討できます。

field の対象となる NULL 値の許容属性

次の既定値を導入することにより、手続間解析をまったく必要とせずに、妥当なレベルの null 安全性を実現することができます。

  1. field 変数には、プロパティと同じ null 許容注釈が常に含まれます。
  2. null 許容属性 [field: MaybeNull, AllowNull] などを使用して、バッキング フィールドの null 許容をカスタマイズできます。
  3. フィールドにサポートされたプロパティは、フィールドの NULL 許容注釈と属性に基づいて、コンストラクターで初期化がチェックされます。
  4. フィールドに基づくプロパティのセッターはコンストラクターと同様に field の初期化をチェックします。

つまり、「ちょっとした怠け者シナリオ」は次のようになります。

class C
{
    public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.

    [field: AllowNull, MaybeNull]
    public string Prop => field ??= GetPropValue();
}

私たちがここでヌル許容属性を使用しない一因は、それらが実際にはシグネチャの入力と出力の説明に重点を置いているためです。 有効期間の長い変数の null 許容を記述するために使用するのは面倒です。

  • 実際には、フィールドを null 許容変数として "合理的に" 動作させるために [field: MaybeNull, AllowNull] が必要です。これにより、null の可能性あり初期フロー状態が得られ、null 値が書き込まれる可能性があります。 比較的一般的な「ちょっとした怠け者」のシナリオでは、ユーザーにこれを実行するよう求めるのは面倒に感じます。
  • このアプローチを追求した場合は、[field: AllowNull] を使用するときに警告を追加することを検討し、MaybeNull も追加することをお勧めします。 これは、AllowNull 自体が null 許容変数からユーザーが必要とするものを行わないためです。フィールドへの書き込みがまだ見られない場合、フィールドは最初は null でないと想定されます。
  • また、[field: MaybeNull] が暗黙的に存在するかのように、field キーワード (または一般的なフィールド) に対する AllowNull の動作を調整して、変数にも null を書き込むこともできます。

の初期フロー状態を推論する field

fieldの null 許容注釈の観点から動作を定義する代わりに、'get' アクセサーでfieldの初期フロー状態を定義できます。 これは、可能な null 値を fieldに割り当てても問題ないという現実を反映します。 私たちが持っている他の形式の分析は、結果として生じる null 安全性の問題を特定することに成功します。

しかし、これは、この変更を行うチャーンが価値があるとは考えられなかった実装の段階で発生しました。 これは、将来的に内部で調整できる可能性があり、中断を認識するユーザーはほとんどいません。この代替アプローチは、一般的に実際に実装したアプローチよりも緩やかです。

回答した LDM の質問

キーワードの構文の位置

fieldvalue が合成されたバッキング フィールドまたは暗黙的なセッター パラメーターにバインドできるアクセサーでは、どの構文の場所で識別子をキーワードと見なす必要がありますか?

  1. いつも
  2. 基本的な表現のみ
  3. never

最初の 2 つのケースは破壊的変更です。

識別子が常にキーワードと見なされる場合、たとえば次のような場合には重大な変更となります。

class MyClass
{
    private int field;
    public int P => this.field; // error: expected identifier

    private int value;
    public int Q
    {
        set { this.value = value; } // error: expected identifier
    }
}

識別子がプライマリ式としてのみ使用される場合にキーワードである場合、破壊的変更は小さくなります。 最も一般的な中断は、「field」という名前の既存メンバーを無修飾で使用することです。

class MyClass
{
    private int field;
    public int P => field; // binds to synthesized backing field rather than 'this.field'
}

また、入れ子になった関数で field または value が再宣言されると、中断が発生します。 これは、value に対する唯一の中断である可能性があります。

class MyClass
{
    private IEnumerable<string> _fields;
    public bool HasNotNullField
    {
        get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
    }
    public IEnumerable<string> Fields
    {
        get { return _fields; }
        set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
    }
}

識別子がキーワードと見な されない 場合、識別子は、他のメンバーにバインドされていない場合にのみ、合成されたバッキング フィールドまたは暗黙的なパラメーターにバインドされます。 この場合、破壊的変更はありません。

答え

field は、 プライマリ式 としてのみ使用される場合、適切なアクセサーのキーワードです。 value はキーワードとは見なされません。

{ set; } に類似したシナリオ

{ set; } は現在許可されていません。これは理にかなっています。これが作成するフィールドを読み取ることは決してできません。 現在、新しい方法が現れており、セッターが { set; }{ set => field = value; }に拡張するなど、読み取られることのないバッキングフィールドを導入する状況に陥ることがあります。

これらのシナリオのうち、コンパイルを許可する必要があるシナリオはどれですか? "フィールドが読み取られることはない" という警告が、手動で宣言されたフィールドと同じように適用されると見なされます。

  1. { set; } - 今日は許可されていない、継続して許可しない
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        get => unrelated;
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    

答え

現在、自動プロパティで既に禁止されている本文なしの set; だけを禁止します。

イベント アクセサーの field

イベント アクセサーで field をキーワードにすべきか、そしてコンパイラがバックフィールドを生成すべきでしょうか?

class MyClass
{
    public event EventHandler E
    {
        add { field += value; }
        remove { field -= value; }
    }
}

推奨事項: field はイベント アクセサー内のキーワード ではなく 、バッキング フィールドは生成されません。

答え

推奨を受け入れました。 field はイベント アクセサー内のキーワード ではなく 、バッキング フィールドは生成されません。

field の NULL 値の許容

提案された field の NULL 値の許容を受け入れるべきですか? 「Nullability」セクションと、その中の未解決の質問を参照してください。

答え

一般的な提案が採用されます。 特定の動作には、さらにレビューが必要です。

プロパティ初期化子の field

プロパティ初期化子で field がキーワードとなり、バッキングフィールドにバインドされるべきか?

class A
{
    const int field = -1;

    object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}

初期化子のバッキング フィールドを参照するための便利なシナリオはありますか?

class B
{
    object P2 { get; } = (field = 2);        // error: initializer cannot reference instance member
    static object P3 { get; } = (field = 3); // ok, but useful?
}

上記の例では、バッキング フィールドにバインドすると、"初期化子は非静的フィールドを参照できません" というエラーが発生します。

答え

以前のバージョンの C# と同様に初期化子をバインドします。 バッキング フィールドをスコープに配置することも、field という名前の他のメンバーを参照することもありません。

部分プロパティとのインタラクション

初期化子

部分プロパティが fieldを使用する場合、初期化子を持つことが許可される必要がある部分はどれですか?

partial class C
{
    public partial int Prop { get; set; } = 1;
    public partial int Prop { get => field; set => field = value; } = 2;
}
  • 両方の部分に初期化子がある場合にエラーが発生する必要があるようです。
  • 定義または実装部分のいずれかが field の初期値を設定する必要があるユース ケースを考えることができます。
  • 定義部分で初期化子を許可すると、プログラムを有効にするために実装者が field を使用することが実質的に強制されているようです。 これについては問題ありませんか?
  • 同じ型のバッキング フィールドが実装で必要な場合は常に、ジェネレーターで field を使用するのが一般的であると考えています。 これは、ジェネレーターが多くの場合、ユーザーがプロパティ定義部分で [field: ...] ターゲット属性を使用できるようにする必要があるためです。 field キーワードを使用すると、このような属性を生成されたフィールドに転送し、プロパティに対する警告を抑制する手間をジェネレーターの実装者から省くことができます。 これらの同じジェネレーターでは、ユーザーがフィールドの初期値を指定できるようにする必要がある可能性もあります。

推奨事項: 実装部分で field を使用する場合は、部分プロパティのいずれかの部分で初期化子を許可します。 両方の部分に初期化子がある場合はエラーを報告します。

答え

推奨事項は受け入れ済みです。 プロパティの場所を宣言または実装する場合は、初期化子を使用できますが、同時に両方を使用することはできません。

自動アクセサー

最初に設計したように、部分プロパティの実装には、すべてのアクセサーの本体が必要です。 ただし、field キーワード機能の最近のイテレーションには、"自動アクセサー" という概念が含まれています。 部分プロパティ実装でこのようなアクセサーを使用できるようにする必要がありますか? それらが排他的に使用されている場合、定義宣言とは区別できません。

partial class C
{
    public partial int Prop0 { get; set; }
    public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.

    public partial int Prop1 { get; set; }
    public partial int Prop1 { get => field; set; } // is this a valid implementation part?

    public partial int Prop2 { get; set; }
    public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?

    public partial int Prop3 { get; }
    public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.

推奨事項: 部分的なプロパティ実装では自動アクセサーを禁止します。これは、使用可能になるタイミングに関する制限は、許可する利点よりも混乱を招くためです。

答え

少なくとも 1 つの実装アクセサーを手動で実装する必要がありますが、その他のアクセサーは自動的に実装できます。

読み取り専用フィールド

合成されたバッキング フィールドを 読み取り専用と見なす必要があるのはいつですか?

struct S
{
    readonly object P0 { get => field; } = "";         // ok
    object P1          { get => field ??= ""; }        // ok
    readonly object P2 { get => field ??= ""; }        // error: 'field' is readonly
    readonly object P3 { get; set { _ = field; } }     // ok
    readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}

バッキング フィールドが 読み取り専用と見なされると、メタデータに出力されるフィールドは initonlyマークされ、初期化子またはコンストラクター以外の field が変更された場合はエラーが報告されます。

推奨事項: 合成されたバッキング フィールドは、包含する型がで、プロパティまたは包含型がstruct宣言されている場合はreadonlyです。

答え

推奨事項は受け入れ済みです。

読み取り専用のコンテキストと set

set を使用するプロパティの readonly コンテキストで field アクセサーを許可する必要がありますか?

readonly struct S1
{
    readonly object _p1;
    object P1 { get => _p1; set { } }   // ok
    object P2 { get; set; }             // error: auto-prop in readonly struct must be readonly
    object P3 { get => field; set { } } // ok?
}

struct S2
{
    readonly object _p1;
    readonly object P1 { get => _p1; set { } }   // ok
    readonly object P2 { get; set; }             // error: auto-prop with set marked readonly
    readonly object P3 { get => field; set { } } // ok?
}

答え

このシナリオでは、set 構造体に readonly アクセサーを実装し、それを渡すかスローする場合があります。 これを許可する予定です。

[Conditional] コード

fieldが条件付きメソッドの省略された呼び出しでのみ使用されている場合、合成されたフィールドを生成する必要がありますか?

たとえば、DEBUG 以外のビルドでは、次のバッキング フィールドを生成する必要がありますか?

class C
{
    object P
    {
        get
        {
            Debug.Assert(field is null);
            return null;
        }
    }
}

参照については、 プライマリ コンストラクター パラメーター のフィールドも同様のケースで生成されます。 sharplab.io を参照してください。

推奨事項: バッキング フィールドは、fieldが条件付きメソッドの省略された呼び出しでのみ使用される場合に生成されます。

答え

Conditional コードは、nullability を変更する Debug.Assert など、非条件付きコードに影響を及ぼす場合があります。 field に似た影響がないのは奇妙なことです。 また、ほとんどのコードで出てくる可能性は低いので、簡単なことを行い、推奨事項を受け入れます。

インターフェイス プロパティと自動アクセサー

自動実装アクセサーが合成されたバッキング フィールドを参照する interface プロパティに対して、手動で実装されたアクセサーと自動実装アクセサーの組み合わせが認識されますか?

インスタンス プロパティの場合、インスタンス フィールドがサポートされていないことを示すエラーが報告されます。

interface I
{
           object P1 { get; set; }                           // ok: not an implementation
           object P2 { get => field; set { field = value; }} // error: instance field

           object P3 { get; set { } } // error: instance field
    static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}

推奨事項: オートアクセサーは interface プロパティで認識され、自動アクセサーは合成されたバッキング フィールドを参照します。 インスタンス プロパティの場合、インスタンス フィールドがサポートされていないことを示すエラーが報告されます。

答え

エラーの原因であるインスタンス フィールド自体を標準化することは、クラスの部分プロパティと一致しており、その結果を気に入っています。 推奨事項は受け入れ済みです。