部分属性

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。

可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/6420

语法

property_declaration 语法 (§14.7.1) 已更新如下:

property_declaration
-    : attributes? property_modifier* type member_name property_body
+    : attributes? property_modifier* 'partial'? type member_name property_body
    ;  

备注:这与指定 方法头(§15.6.1)类声明(§15.2.1) 有些相似。 (请注意,问题 #946 建议放宽排序要求,并可能适用于允许 partial 修饰符的所有声明。我们打算在不久的将来指定这样的排序放松,并在实现此功能的同一版本中实现它。

定义和实现声明

当属性声明包含 部分 修饰符时,该属性称为 部分属性。 部分属性只能声明为局部类型的成员。

分部属性声明的访问器都有分号主体,并且缺少 extern 修饰符时,它被称为定义声明。 否则,它是实现声明

partial class C
{
    // Defining declaration
    public partial string Prop { get; set; }

    // Implementing declaration
    public partial string Prop { get => field; set => field = value; }
}

由于我们已为定义声明保留分号访问器主体的语法形式,因此无法自动实现分部属性。 因此,我们对 自动实现的属性(§15.7.4) 做如下调整:

自动实现的属性(简称自动属性)是一个非抽象、非外部、非分部、非引用值的属性,其访问器主体仅包含分号。

备注。 编译器能够单独查看单个声明并知道它是定义声明还是实现声明,这非常有用。 因此,例如,我们不希望通过包括两个相同的 partial 属性声明来允许自动属性。 我们不认为此功能的用例涉及用自动属性实现分部属性,但在想要实现简单的情况下,我们认为 field 关键字足够简化了这一过程。


分部属性必须有一个 定义声明,一个 实现声明

备注。 我们也不认为允许将声明拆分为两个以上的分部是有用的,例如,允许在不同的地方实现不同的访问器。 因此,我们仅仅模仿由分部方法建立的方案。


只有分部属性的定义声明参与查找,类似于仅分部方法的定义声明参与重载解析的方式。

备注。 在编译器中,我们预计只有定义声明的符号出现在成员列表中,并且实现部件的符号可以通过定义符号访问。 但是,某些功能(如可为 null 分析)可能会通过查看来实现声明,以提供更有用的行为。

partial class C
{
    public partial string Prop { get; set; }
    public partial string Prop { get => field; set => field = value; }

    public C() // warning CS8618: Non-nullable property 'Prop' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
    {
    }
}

不允许分部属性具有 abstract 修饰符。

分部属性无法显式实现接口属性。

属性合并

与分部方法类似,生成的属性中的属性是部分的组合属性以未指定的顺序串联,并且不会删除重复项。

调用方信息属性

我们根据标准调整了以下内容:

在定义和实现分部方法成员声明的参数上使用相同的调用方信息属性是错误的。 仅应用定义部件中的调用方信息属性,而仅出现在实现部件中的调用方信息属性将被忽略。

  • 所描述的错误源于这些没有 AllowMultiple = true 的属性的定义。 使用它们多次(包括跨部分声明)会导致错误。
  • 当调用方信息属性应用于分部方法的实现分部中的参数时,Roslyn 编译器将报告警告。 它还将在分部属性中报告相同情况的警告。

匹配签名

2020 年 9 月 14 日的 LDM 会议为分部方法的特征匹配定义了一套“严格”的要求,这些要求是在警告波中引入的。 分部属性对签名匹配的分部方法有尽可能多的类似要求,除了默认情况下会报告所有不匹配的诊断,并且不会隐藏在警告波之后。

签名匹配要求包括:

  1. 部分属性声明的类型和 ref 类型差异对运行时非常重要,会导致编译时错误。
  2. 部分属性声明中元组元素名称的差异会导致编译时错误,与分部方法相同。
  3. 属性声明及其访问器声明必须具有相同的修饰符,但修饰符可能以不同的顺序显示。
    • 异常:这不适用于 extern 修饰符,该修饰符可能只出现在实现声明 上。
  4. 部分属性声明签名中的所有其他语法差异都会导致编译时警告,但有以下例外:
    • 分部属性声明中的属性列表不需要匹配。 相反,根据属性合并,将执行相应位置中的属性合并。
    • 可为 Null 的上下文差异不会导致警告。 换句话说,如果一种类型是 nullable-oblivious,而另一种类型是 nullable-annotated 或 not-nullable-annotated,则不会产生任何警告。
    • 默认参数值不需要匹配。 当部分索引器的实现部分具有默认参数值时,将报告警告。 这类似于当分部方法的实现部分具有默认参数值时发生的现有警告。
  5. 当定义和实现声明的参数名称不同时,会出现警告。 定义部分的参数名称在使用站点和发出中使用。
  6. Null 性差异(不涉及未知 Null 性)会导致警告。 分析访问器正文时,将使用实现部分签名。 在分析使用站点和发出时使用定义部分签名。 这与分部方法一致。
partial class C1
{
    public partial string Prop { get; private set; }

    // Error: accessor modifier mismatch in 'set' accessor of 'Prop'
    public partial string Prop { get => field; set => field = value; }
}

partial class C2
{
    public partial string Prop { get; init; }

    // Error: implementation of 'Prop' must have an 'init' accessor to match definition
    public partial string Prop { get => field; set => field = value; }
}

partial class C3
{
    public partial string Prop { get; }

    // Error: implementation of 'Prop' cannot have a 'set' accessor because the definition does not have a 'set' accessor.
    public partial string Prop { get => field; set => field = value; }
}

partial class C4
{
    public partial string this[string s = "a"] { get; set; }
    public partial string this[string s] { get => s; set { } } // ok

    public partial string this[int i, string s = "a"] { get; set; }
    public partial string this[int i, string s = "a"] { get => s; set { } } // CS1066: The default value specified for parameter 's' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments
}

文档注释

我们希望分部属性上的文档注释行为与我们为分部方法提供的行为一致。 https://github.com/dotnet/csharplang/issues/5193中详细介绍了该行为。

允许对部分属性的定义或实现部分包含文档注释。 (请注意,属性访问器不支持文档注释。)

当文档注释仅存在于属性的某一部分时,这些注释将被正常使用(例如,通过 ISymbol.GetDocumentationCommentXml() 被展示,并写入到文档 XML 文件中)。

当文档注释同时存在于这两个部分时,将删除定义部件上的所有文档注释,并且仅使用实现部件上的文档注释。

例如,以下程序:

/// <summary>
/// My type
/// </summary>
partial class C
{
    /// <summary>Definition part comment</summary>
    /// <returns>Return value comment</returns>
    public partial int Prop { get; set; }
    
    /// <summary>Implementation part comment</summary>
    public partial int Prop { get => 1; set { } }
}

生成以下 XML 文档文件:

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>ConsoleApp1</name>
    </assembly>
    <members>
        <member name="T:C">
            <summary>
            My type
            </summary>
        </member>
        <member name="P:C.Prop">
            <summary>
            Implementation part comment
            </summary>
        </member>
    </members>
</doc>

当部分声明之间的参数名称不同时,<paramref> 元素使用与源代码中的文档注释关联的声明的参数名称。 例如,放置在实现声明上的文档注释中的参数引用,通过参数名称指向实现声明中的参数符号。 这与分部方法一致。

/// <summary>
/// My type
/// </summary>
partial class C
{
    public partial int this[int x] { get; set; }

    /// <summary>
    /// <paramref name="x"/> // warning CS1734: XML comment on 'C.this[int]' has a paramref tag for 'x', but there is no parameter by that name
    /// <paramref name="y"/> // ok. 'Go To Definition' will go to 'int y'.
    /// </summary>
    public partial int this[int y] { get => 1; set { } } // warning CS9256: Partial property declarations 'int C.this[int x]' and 'int C.this[int y]' have signature differences.
}

生成以下 XML 文档文件:

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>ConsoleApp1</name>
    </assembly>
    <members>
        <member name="T:C">
            <summary>
            My type
            </summary>
        </member>
        <member name="P:C.Item(System.Int32)">
            <summary>
            <paramref name="x"/> // warning CS1734: XML comment on 'C.this[int]' has a paramref tag for 'x', but there is no parameter by that name
            <paramref name="y"/> // ok. 'Go To Definition' will go to 'int y'.
            </summary>
        </member>
    </members>
</doc>

这可能会令人困惑,因为元数据签名将使用定义部件中的参数名称。 建议确保参数名称在各个部分之间匹配,以避免这种混淆。

索引器

根据 2022 年 11 月 2 日的 LDM 会议,索引器将支持此功能。

索引器语法的修改方式如下:

indexer_declaration
-    : attributes? indexer_modifier* indexer_declarator indexer_body
+    : attributes? indexer_modifier* 'partial'? indexer_declarator indexer_body
-    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
+    | attributes? indexer_modifier* 'partial'? ref_kind indexer_declarator ref_indexer_body
    ;

分部索引器参数必须按照与匹配签名相同的规则在声明之间匹配。 属性合并在分部索引器参数之间执行。

partial class C
{
    public partial int this[int x] { get; set; }
    public partial int this[int x]
    {
        get => this._store[x];
        set => this._store[x] = value;
    }
}

// attribute merging
partial class C
{
    public partial int this[[Attr1] int x]
    {
        [Attr2] get;
        set;
    }

    public partial int this[[Attr3] int x]
    {
        get => this._store[x];
        [Attr4] set => this._store[x] = value;
    }

    // results in a merged member emitted to metadata:
    public int this[[Attr1, Attr3] int x]
    {
        [Attr2] get => this._store[x];
        [Attr4] set => this._store[x] = value;
    }
}

未解决的问题

其他成员类型

一位社区成员发起了一场讨论,以请求对部分事件的支持。 在 2022 年 11 月 2 日的 LDM 会议上,我们决定暂缓支持活动,部分原因是因为当时没有人要求。 我们可能想重新审视这个问题,因为我们现在收到了这个请求,自从我们上次讨论已经过去了一年多。

我们还可以进一步允许构造函数、运算符、字段等的部分声明,但目前还不清楚这些构造函数的设计负担是否合理,只是因为我们已经在执行部分属性。