属性中的 field 关键字

总结

扩展所有属性,以允许它们使用新的上下文关键字 field引用自动生成的后备字段。 属性现在还可以包含不含主体的访问器和主体的访问器。

动机

自动属性仅允许直接设置或获取支持字段,从而仅通过在访问器上放置访问修饰符来提供一定控制权。 有时,需要对一个或两个访问器中发生的情况进行额外控制,但这会让用户承担声明支持字段的开销。 接着,必须将支持字段名称与属性保持同步,并将支持字段的范围限定为整个类,这可能会导致意外绕过该类中的访问器。

有几个常见方案。 在 getter 中,如果从未设置属性,会有延迟初始化或默认值。 在 setter 中,应用约束来确保值的有效性,或通过引发 INotifyPropertyChanged.PropertyChanged 事件来检测和传播更新。

在这些情况下,你始终必须创建实例字段并自行编写整个属性。 在通常仅希望将其用于访问器的主体时,这不仅会添加相当数量的代码,而且还会将支持字段泄漏到类型的其余范围中。

词汇表

  • 自动属性:“自动实现的属性”的简称 (§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。 通过添加初始 @,在从 14 之前的 C# 版本升级时,在属性访问器主体中声明的名为 field 的所有标识符都可以防止中断。

如果在属性访问器中声明了名为 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 模式提供了优雅的解决方案。 视图模型属性 setter 可能会将数据绑定到 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;如果没有 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 关键字表示的支持字段在与任何其他结构字段相同的条件下,仍然会出现 default-initialization 和 disabled-by-default 警告(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 访问器和属性初始化器能够访问支持字段。 由于目前没有这种情况的用例,现在还不能开始将引用返回属性作为自动属性写入。

可空性

可为空引用类型功能的原则是理解 C# 中的现有惯用编码模式,并尽量减少与这些模式相关的额外工作。 field 关键字建议支持使用简单的惯用模式来解决广泛需求的应用场景,例如延迟初始化的属性。 对于可为空引用类型,与这些新编码模式很好地兼容非常重要。

目标:

  • 应为 field 关键字功能的各种使用模式确保合理的 null 安全级别。

  • 使用 field 关键字的模式应该感觉就像它们一直是语言的一部分。 避免让用户费力地启用代码中非常适用于 field 关键字功能的可为空引用类型。

其中一个主要应用场景是延迟初始化的属性:

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

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

以下空性规则不仅适用于使用 field 关键字的属性,还适用于现有的自动属性。

支持字段的可为空性

有关新术语的定义,请参阅 术语表

后备字段 的类型与属性相同。 但是,其可为空注释可能与属性不同。 为了确定此可为空注释,我们引入了 null 弹性概念。 null 弹性从直观上讲,意味着即使字段包含其类型的 get 值,属性的 default 访问器仍能维护 null 安全性。

字段支持的属性被确定为具有 null 弹性,或者并非通过执行其 get 访问器的特殊可为空分析来实现。

  • 出于此分析的目的,暂时假定 field 具有 annotated 可为空性,例如 string?。 这会导致 fieldget 访问器中具有 maybe-nullmaybe-default 初始状态,具体取决于其类型。
  • 然后,如果 getter 的可为空分析不会生成任何可为空警告,则该属性将具有 null 弹性。 否则,就不具有 null 弹性
  • 如果属性没有 get 访问器,则它(空虚地)具有 null 弹性。
  • 如果自动实现了 get 访问器,则属性不具有 null 弹性。

支持字段的可为空性可按如下方式确定:

  • 如果字段具有可为 null 属性(如 [field: MaybeNull]AllowNullNotNullDisallowNull),则该字段的可为 null 批注与该属性的可为 null 批注相同。
    • 这是因为当用户将可为空性特性应用于字段时,我们不再希望推断任何内容,我们只希望可为空性如用户所述
  • 如果包含属性具有 obliviousannotated 可为空性,则支持字段的可为空性与属性相同。
  • 如果包含属性具有 not-annotated 可为空性(例如 stringT),或者具有 [NotNull] 特性,并且属性具有 null 弹性,则支持字段具有 annotated 可为空性。
  • 如果包含属性具有 not-annotated 可为空性(例如 stringT),或者具有 [NotNull] 特性,并且属性不具有 null 弹性,则支持字段具有 not-annotated 可为空性。

构造函数分析

目前,在可为空构造函数分析中,自动属性的处理方式与普通字段非常相似。 我们将这种处理扩展到字段支持的属性,方法是将每个字段支持的属性都作为其支持字段的代理进行处理。

我们将更新上述建议方法中的以下规范语言,以便实现此目的:

在构造函数的每个显式或隐式“返回”处,如果有成员的流状态与其注释和可为空性特性不兼容,我们会发出警告。 如果该成员是字段支持的属性,则支持字段的可为空注释将用于此检查。 否则,将使用成员本身的可为空注释。 这方面的合理代理是:如果在返回点将成员分配给自身会产生可为空性警告,那么在返回点将会生成可为空性警告。

请注意,这实质上是约束的跨过程分析。 我们预计,为了分析构造函数,必须对同一类型中所有适用的 get 访问器执行绑定和“null 弹性”分析,这些访问器使用 field 上下文关键字,并且具有 not-annotated 可为空性。 我们推测,这并不昂贵,因为 getter 主体通常并不十分复杂,并且无论类型中有多少个构造函数,都仅需要执行“null 复原”分析一次。

setter 分析

为简单起见,我们使用术语“setter”和“set 访问器”来指代 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 中的每个显式或隐式“返回”处,如果支持字段的流状态与其注释和可为空性特性不兼容,则会报告警告。

言论

此表述有意与构造函数中的普通字段非常相似。 实质上,由于只有属性访问器能够实际引用支持字段,因此 setter 被视为支持字段的“微型构造函数”。

与普通字段类似,我们通常可以知道属性是在构造函数中进行初始化的,因为它已经被设置,但这并不总是如此。 仅在 Prop != null 为 true 的分支中返回也足以进行构造函数分析,因为我们知道未跟踪的机制可能已用于设置属性。

已考虑替代方案;请参阅可为空性替代方案部分。

nameof

field 是关键字的位置,nameof(field) 将无法编译(LDM 决策),如 nameof(nint)。 它与 nameof(value) 不同,后者是要在属性 setter 引发 ArgumentException 时使用的内容,就像 .NET 核心库中一样。 相比之下,nameof(field) 没有预期的用例。

覆盖

重写属性可以使用 fieldfield 的此类用法是指重写属性的支持字段,与基属性的支持字段(如有)不同。 没有 ABI 可用于将基属性的支持字段向重写类公开,因为这会中断封装。

与自动属性一样,使用 field 关键字和重写基属性的属性必须重写所有访问器(LDM 决策)。

捕获

field 应该能够在本地函数和 lambda 中被捕获,并且即使没有其他引用,也允许从本地函数和 lambda 内部引用 fieldLDM 决策 1LDM 决策 2):

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

字段用法警告

当访问器中使用 field 关键字时,编译器对未分配或未读字段的现有分析将包含该字段。

  • CS0414:为属性“Xyz”分配后盾字段,但永远不会使用其值
  • CS0649:属性“Xyz”的支持字段从未被分配,因此将始终保留默认值。

规范更改

语法

当使用语言版本 14 或更高版本进行编译时,在以下位置(LDM 决策)用作 主表达式LDM 决策)时,field 被视为一个关键字:

  • 在属性但不在索引器中的 getsetinit 访问器的方法主体中
  • 应用于这些访问器的属性中
  • 在嵌套的 lambda 表达式中、在本地函数中,以及在这些访问器的 LINQ 表达式中

在所有其他情况下,包括使用语言版本 12 或更低版本进行编译时,field 被视为标识符。

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

性能

§15.7.1属性 - 常规

只能为自动实现的属性以及具有将发出的支持字段的属性中提供 property_initializerproperty_initializer 会导致使用表达式给出的值初始化此类属性的基础字段。

§15.7.4自动实现的属性

自动实现的属性(简称自动属性)是一个非抽象、非外部的非引用值属性,具有仅分号访问器主体。自动属性应具有 get 访问器,并且可以选择性地具有 set 访问器。以下任一项或两项:

  1. 具有仅分号主体的访问器
  2. 访问器中的 field 上下文关键字或属性的表达式主体的用法

将属性指定为自动实现的属性时,未命名支持字段会自动可用于属性 ,并且将实现访问器从该支持字段读取和写入该支持字段对于自动属性,任何仅分号 get 访问器都将实现为从其支持字段读取,而任何仅分号set访问器都将实现为写入其支持字段。

隐藏后盾字段不可访问,它只能通过自动实现的属性访问器(即使在包含类型内)读取和写入。可以使用所有访问器和属性表达式正文中的 field 关键字直接引用后备字段。由于字段未命名,因此不能在nameof 表达式中使用。

如果自动属性没有 set 访问器仅有仅分号 get 访问器,则支持字段将被视为 readonly (§15.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 弹性方法(在可为空性部分中概述)外,工作组还建议以下替代方案供 LDM 考虑:

不做任何事情

我们根本无法在此处引入任何特殊行为。 实际上:

  • 以当前处理自动属性的方式处理字段支持的属性 - 必须在构造函数中进行初始化,但标记为必需等除外。
  • 分析属性访问器时,不对字段变量进行特殊处理。 它只是一个与属性类型相同并且具有可为空特性的变量。

请注意,这将导致“延迟属性”应用场景出现不必要的警告,在这种情况下,用户可能需要分配 null! 或类似的值来抑制构造函数警告。
我们可以考虑的一个“次要替代方案”也是完全忽略使用 field 关键字的属性,以便进行可为空构造函数分析。 在这种情况下,关于用户需要初始化的任何警告都不存在,但不管他们使用什么初始化模式,也不会对用户造成任何困扰。

由于我们仅计划将 field 关键字功能置于 .NET 9 的预览版 LangVersion 下,因此我们希望在 .NET 10 中能够修改该功能的可为空性行为。 因此,我们可以考虑在短期内采用这样的“低成本”解决方案,并在长期内成长为更复杂的解决方案之一。

field-目标可为空性特性

我们可以引入以下默认值,实现合理的 null 安全级别,而不涉及任何过程间分析:

  1. field 变量始终具有与属性相同的可为空注释。
  2. 可为空性特性 [field: MaybeNull, AllowNull] 等可用于自定义支持字段的可为空性。
  3. 在构造函数中,根据字段的可为空注释和特性,检查字段支持的属性是否已初始化。
  4. 字段支持的属性中的 setter 会检查 field 是否已初始化,这类似于构造函数。

这意味着“little-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 的值。 这感觉很繁琐,要求用户为一些相对常见的“little-l 延迟”应用场景执行操作。
  • 如果我们采用此方法,我们会考虑在使用 [field: AllowNull] 时添加警告,建议同时添加 MaybeNull。 这是因为 AllowNull 本身不会执行用户需要从可为空变量中获取的内容:它假定该字段最初非 null(当我们从未看到该变量的任何写入时)。
  • 我们还可以考虑调整 field 关键字上 [field: MaybeNull] 的行为,甚至调整一般字段的行为,以允许 null 也能写入变量,就好像 AllowNull 也隐式存在一样。

已回答的 LDM 问题

关键字的语法位置

在访问器中,当 fieldvalue 可以绑定到合成的支持字段或隐式 setter 参数时,在哪些语法位置应将标识符视为关键字?

  1. 通用
  2. 主表达式
  3. never

前两种情况是中断性变更。

如果标识符始终被视为关键字,则这是以下项的中断性变更,例如:

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 的可为空性建议? 请参阅可为空性部分,以及其中未回答的问题。

通过了总体提案。 特定行为仍需要更多审查。

属性初始化器中的 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.

建议:在部分属性实现中禁止自动访问器,因为有关何时使用自动访问器的限制比允许它们的好处更令人困惑。

必须至少手动实现一个访问器,而另一个访问器可以自动实现。

只读字段

何时应将合成支持字段视为只读

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

对于使用 readonly 的属性,是否应允许在 field 上下文中存在 set 访问器?

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

可能会出现这种应用场景,即正在 readonly 结构上实现 set 访问器,并传递它或引发。 我们将允许这样做。

[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 属性中识别自动访问器,并且自动访问器应用合成支持字段。 对于实例属性,会报告一个错误,即不支持实例字段。

围绕实例字段本身进行标准化作为错误原因,与类中的部分属性保持一致,我们也希望是这种结果。 接受该建议。