共用方式為


屬性中的 field 關鍵詞

冠軍問題:https://github.com/dotnet/csharplang/issues/8635

總結

擴充所有屬性,以允許它們使用新的內容關鍵詞 field參考自動產生的備份欄位。 屬性現在也可以包含存取子 ,而不 主體與存取子 主體。

動機

自動屬性只允許直接設定或取得後備欄位,僅能通過在存取子上放置存取修飾詞來給予某些控制。 有時候需要在一或兩個存取子中對發生的情況有更多控制,但這會讓使用者面臨必須宣告後備欄位的額外負擔。 接著,備份功能變數名稱必須與屬性保持同步,且備份欄位的範圍設定為整個類別,這可能會導致意外略過 類別內的存取子。

有數個常見的案例。 在 getter 中,當從未指定 屬性時,會有延遲初始化或預設值。 在 setter 中,會套用條件約束以確保值的有效性,或藉由引發 INotifyPropertyChanged.PropertyChanged 事件來偵測和傳播更新。

在這些情況下,您一律必須建立實例字段,並自行撰寫整個屬性。 這不僅會增加相當數量的程序代碼,也會將備份欄位洩漏到型別範圍的其餘部分,而通常最好只讓存取子的主體使用。

詞彙表

  • Auto 屬性:「自動實作屬性」的縮寫(§15.7.4)。 自動屬性上的存取子沒有主體。 編譯程式提供實作和後援儲存。 自動屬性具有 { get; }{ get; set; }{ get; init; }

  • 自動存取子:「自動實作存取子」的簡稱。這是一種沒有實際程式碼的存取子。 編譯程式提供實作和後援儲存。 get;set;init; 是自動存取器。

  • 完整存取子:這是具有身體的完整存取子。 編譯程式不會提供實作,不過基礎存儲可能仍然會提供(如範例 set => field = value;所示)。

  • 欄位支援的屬性:這是使用存取子內的 field 關鍵字的屬性,或是一個自動屬性。

  • 備份欄位:這是屬性存取子中由 field 關鍵詞表示的變數,它也會以隱含方式讀取或寫入自動實作存取子 (get;set;init;)。

詳細設計

對於具有 init 存取子的屬性,以下適用於 set 的所有內容都將改為適用於 init 存取子。

語法有兩個變更:

  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(); }

僅可設定的屬性也可以使用 field

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

重大變更

屬性存取器內部 field 這個上下文關鍵字的存在,可能會引發重大的變更。

因為 field 是關鍵詞,而不是標識符,所以只能使用一般關鍵詞逸出路由的標識碼「遮蔽」:@field。 在屬性存取子主體中宣告的所有名為 field 的標識符,可以透過添加初始 @,避免在從低於14版本的C#升級時出現問題。

如果在屬性存取子中宣告名為 field 的變數,則會報告錯誤。

在語言版本 14 或更新版本中,如果 主要表達式field 參考支援欄位,但先前的語言版本會參考不同的符號,則會報告警告。

欄位目標屬性

如同自動屬性,在其其中一個存取子中使用備份欄位的任何屬性都能夠使用欄位目標屬性:

[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。 備份欄位會直接初始化,而不是呼叫 setter(LDM 決策)。

呼叫初始化表達式的 setter 不是選項;初始化表達式會在呼叫基底建構函式之前進行處理,而且呼叫基底建構函式之前呼叫任何實例方法並不合法。 對於結構的默認初始化/明確指派而言,這也很重要。

這會產生對初始化的彈性控制。 如果您想要在不呼叫 setter 的情況下初始化,請使用屬性初始化器。 如果您想要藉由呼叫 setter 來初始化,請使用 在建構函式中將初始值指派給 屬性。

以下是其中很有用的範例。 我們相信,field 關鍵詞將在與檢視模型搭配使用時發揮很大作用,因為它為 INotifyPropertyChanged 模式提供了一個優雅的解決方案。 檢視模型屬性設定器可能數據繫結至 UI,並可能會導致變更記錄或觸發其他行為。 下列程式代碼需要初始化 IsActive 的預設值,而不將 HasPendingChanges 設定為 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 (RuntimeHelpers.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");
        }
    }
}

建構函式指派

如同自動屬性,建構函式中的指派會在存在時呼叫 (可能虛擬) setter,如果沒有 setter,則會回復為直接指派給備份欄位。

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 關鍵詞將無法用於 ref-returning 屬性。 Ref 傳回屬性不能有 set 存取子,沒有 set 存取子的情況下,get 存取子和屬性初始化器將是唯一可以存取備援欄位的部分。 現在沒有這方面的使用案例,因此不是開始將返回引用的屬性寫成自動屬性的時候。

可空性

可為 Null 參考型別功能的原則是瞭解 C# 中現有的慣用編碼模式,並且在實現這些模式時,儘量減少繁複的步驟。 field 關鍵詞提案使簡單且慣用的模式能夠解決經常被要求的情境,例如延遲初始化的屬性。 務必讓可空參考型別與新的程式編碼模式相容。

目標:

  • 對於 field 關鍵詞功能的各種使用模式,應確保合理的空值安全層級。

  • 使用 field 關鍵詞的模式應該感覺就像一直是語言的一部分。 避免讓使用者煞費苦心地啟用在對 field 關鍵字功能而言完全符合語意的程式碼中的可空參考型別。

其中一個主要案例是延遲初始化的屬性:

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

  • 為了進行這項分析,暫時假設 field註解具有 可為 Null 的特性,例如 string?。 這會導致在 field 存取子中, 依據其類型,初始狀態可能為 nullget
  • 然後,如果 getter 的可空性分析不會產生可空性的警告,則該屬性具備 null 復原性。 否則,它不會 null 復原
  • 如果屬性沒有 get 存取子,它是(理論上)具備 null 回復能力的。
  • 如果 get 存取子是自動實作的,則屬性沒有 Null 韌性能力。

後設欄位的可空性決定於下列方式:

  • 如果欄位具有 null 屬性,例如 [field: MaybeNull]AllowNullNotNullDisallowNull,則欄位的可為 Null 註釋與屬性的可為 Null 註釋相同。
    • 這是因為當用戶開始將 nullability 屬性套用至欄位時,我們不想再推斷其他任何事情,我們只想要 使用者所說的
  • 如果包含的屬性具有難察覺的批註的 可為 Null 特性,那麼後援欄位的可為 Null 性將與屬性相同。
  • 如果包含的屬性具有 非批註 可為 Null 功能(例如 stringT),或具有 [NotNull] 屬性,而且屬性 具有 null 復原,則備份欄位 批註 可為 Null。
  • 如果包含的屬性 未標註的 可為 Null 性(例如 stringT)或具有 [NotNull] 屬性,且該屬性不具有 null 彈性 ,則備份欄位具有未標註的 可為 Null 性。

建構函式分析

目前,auto 屬性會與 可為 Null 的建構函式分析中的一般欄位處理得非常類似,。 我們藉由將每個 欄位支援的屬性視為其支援欄位的 Proxy,藉此將此處理延伸到 字段支援的屬性

我們會將以下規格語言從先前的 建議方法更新為,以達成此目的:

在建構函式中,每當遇到明確或隱含的 'return' 時,我們會針對那些流程狀態與其註解和 Null 性屬性不相容的成員發出警告。 如果成員是由欄位支援的屬性,則支援欄位的可空註釋會用於此檢查。 否則,將使用成員本身的可空註釋。 一個合理的代理是:如果在回傳點將成員賦值給其本身會產生 Nullability 警告,那麼在回傳點就會產生 Nullability 警告。

請注意,這基本上是一種受限的跨程序分析。 我們預期為了分析建構子,您必須對相同類型中所有適用的 get 存取子執行繫結和「null 耐受性」分析,這些存取子使用 field 內容關鍵詞,且具有 未註記的 空值性。 我們推測這無需耗費過多成本,因為 getter 主體通常不會太複雜,而且不論類型中有多少建構函式,只需執行一次「防範 Null」分析即可。

Setter 分析

為了簡單起見,我們使用「設定子」和「設定存取子」這些術語來指代 setinit 存取子。

需要檢查 使用欄位作為支援的屬性的 setter 是否實際初始化了備援欄位。

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(); // NRE at runtime
    }
}

欄位支援屬性 的 setter 中,支援欄位的初始流程狀態 決定如下:

  • 如果屬性具有初始化運算式,則初始流程狀態會與訪問初始化表達式之後的屬性流程狀態相同。
  • 否則,初始流程狀態與 field = default;所提供的流程狀態相同。

在 setter 中的每個明確或隱含的 'return' 中,如果 後援欄位的流程狀態 與其註釋和可為 Null 的屬性不相容,就會報告警告。

備註

此公式刻意類似於建構函式中的一般欄位。 基本上,因為只有屬性存取子可以實際參考後備欄位,因此 setter 會被視為後備欄位的「小型建構子」。

和一般欄位一樣,我們通常知道屬性是在建構子中初始化的,因為它通常會被設定,但這並非必然如此。 只要在 Prop != null 為 true 的分支內傳回,也足以分析建構函式,因為我們知道未追蹤的機制可能已用來設定 屬性。

已考慮替代方案;請參閱 Nullability 替代辦法 一節。

nameof

field 是關鍵詞的地方,nameof(field) 將無法編譯(LDM 決策),例如 nameof(nint)。 不像 nameof(value),那不是用來處理當屬性設置器丟擲 ArgumentException 時的情況,而這正是 .NET Core 函式庫中某些例子的做法。 相反地,nameof(field) 沒有預期的使用案例。

重寫

覆寫屬性可能會使用 field。 這類 field 用法指的是覆寫屬性的後備欄位,如果基底屬性有後備欄位,則與基底屬性的後備欄位分開。 沒有 ABI 可將基底屬性的備份欄位公開至覆寫類別,因為這會中斷封裝。

像自動屬性一樣,使用 field 關鍵詞並覆寫基底屬性的屬性必須覆寫所有的存取子(LDM 決策)。

捕獲

field 應該能夠在本機函式和 Lambda 中擷取,即使沒有其他參考,也允許從本機函式和 Lambda 中擷取 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 決策)(LDM 決策):

  • 在屬性 getsetinit 存取子的方法主體中,而不是在 的索引器中,
  • 套用於這些存取子的屬性
  • 在巢狀的 Lambda 表達式和區域函數中,以及此類存取子中的 LINQ 表達式

在所有其他情況下,包括使用語言版本 12 或更低版本進行編譯時,field 會被視為標識符。

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

性能

§15.7.1屬性 - 一般

property_initializer 只能提供給自動實作屬性的 ,而且將發出支援欄位的屬性。property_initializer 會使用 表示式所指定的值,來初始化這類屬性的基礎欄位,

•15.7.4自動實作屬性

自動實作的屬性(簡稱為 auto-property)是非抽象、不屬於外部的、非 ref 值屬性,具有 僅限分號的存取子主體。自動屬性應該有 get 存取子,而且可以選擇性地擁有 set 存取子。或兩者:

  1. 具有僅限分號主體的存取子
  2. field 的表示式主體內使用 內容關鍵詞的方式

當屬性指定為自動實作的屬性時,屬性 會自動使用隱藏 未命名的 支援字段,而且存取子會實作以讀取和寫入該支援字段針對自動屬性,會實作任何僅限分號 get 存取子來讀取,以及任何要寫入其支援字段的僅限分號set 存取子。

隱藏的備份欄位無法存取,它只能透過自動實作的屬性存取子來讀取和寫入,即使是在包含的類型內也是如此。可以使用所有存取子和屬性表達式主體內的 field 關鍵詞直接參考備份欄位。因為欄位未命名,所以無法在nameof 表示式中使用。

如果 auto-property 沒有 set 存取子只有分號 get 存取子,則會將備份欄位視為 readonly15.5.3)。 就像 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! 或類似的來消除建構函式警告。
我們可以考慮的次要替代方案是,在使用 field 關鍵詞進行可為 Null 建構函式分析時,完全忽略屬性。 在這種情況下,無論使用者採用何種初始化方式,都不會出現任何需初始化的警告,也不會給使用者帶來任何不便。

由於我們只打算在 .NET 9 的 Preview LangVersion 下發布 field 關鍵詞功能,因此我們預期在 .NET 10 中有能力調整該功能的可為 Null 特性的行為。 因此,我們可以考慮在短期內採用這種「低成本」的解決方案,並且在長期內發展至其中一種更複雜的解決方案。

field- 目標指向空值屬性

我們可以引入下列默認值,達到合理的空值安全性層級,而不需涉及任何程式間分析:

  1. field 變數的空值註釋始終與屬性相同。
  2. 可以使用 Null 性質 [field: MaybeNull, AllowNull] 等,來自定義後援字段的可空性。
  3. 欄位支援的屬性會根據欄位的可為 Null 註釋和屬性,檢查建構函式中的初始化。
  4. 欄位支援屬性中的 setter 會檢查 field 的初始化方式與建構函式類似。

這表示「小寫 l 懶惰情境」會看起來像這樣:

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();
}

我們迴避使用可空性屬性的原因之一是,這些屬性主要用於描述函式簽章的輸入和輸出。 它們使用起來很笨重,不利於描述持續存在變數的空值性。

  • 在實務上,需要 [field: MaybeNull, AllowNull] 使欄位作為可為 Null 的變數進行合理操作,以提供可能為 Null 的初始流動狀態,並允許將可能的 Null 值寫入該欄位。 這樣要求用戶處理相對常見的小懶惰情境,感覺非常麻煩。
  • 如果我們採用這種方法,我們會考慮在使用 [field: AllowNull] 時新增警告,建議同時新增 MaybeNull。 這是因為 AllowNull 本身不能滿足使用者對可為 Null 變數的需求:它假設該欄位在一開始沒有任何數值被寫入時不是 Null。
  • 我們也可以考慮調整 [field: MaybeNull]field 關鍵詞上的行為,或者調整一般欄位,以允許將 null 寫入變數,就好像隱含地也包含了 AllowNull 一樣。

已回答LDM問題

關鍵詞的語法位置

在存取子中,fieldvalue 可以綁定到合成的支援欄位或者隱含的 setter 參數,在這些情況下,應該在什麼語法位置將識別字視為關鍵字?

  1. 總是
  2. 僅主要表示式
  3. 從不

前兩個案例是重大變更。

如果標識子 一律 視為關鍵詞,這將對以下內容造成重大變更,例如:

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'
}

當巢狀函式中重新宣告 fieldvalue 時,也會有中斷。 這可能是 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; } 目前不允許,這很合理:這個建立的欄位永遠無法讀取。 目前有一些新的辦法會導致在 setter 中引入一個從未讀取的備援欄位,例如將 { 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 的可為空性

是否應接受建議的 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.

建議:不允許在部分屬性的實作中使用自動存取子,因為相比允許它們的好處,使用時的限制更讓人困惑。

至少必須手動實作一個實作存取子,但可以自動實作另一個存取子。

Readonly 欄位

何時應該將合成後備欄位視為唯讀

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 程式代碼可能會對非條件性程式代碼產生影響,例如 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 屬性中,自動存取子會被辨識,並且它們會參考自動生成的備用字段。 針對實例屬性,會回報錯誤,表示不支持實例欄位。

將實例欄位本身作為錯誤原因進行標準化,這與類別中的部分屬性保持一致,因此我們對此結果表示滿意。 已接受建議。