扩展分部方法

注意

本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

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

可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。

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

总结

此提案的目的是移除所有关于 C# 中 partial 方法签名的限制。 目标是扩展可以结合源生成器使用的方法场景,同时使其成为C#方法的通用声明形式。

另请参阅部分方法规范原文 (§15.6.9)。

动机

C# 对开发人员将方法分为声明和定义/实现的支持有限。

partial class C
{
    // The declaration of C.M
    partial void M(string message);
}

partial class C
{
    // The definition of C.M
    partial void M(string message) => Console.WriteLine(message);
}

partial 方法的一个特性是,当定义缺失时,语言会直接清除对 partial 方法的任何调用。 从本质上讲,它的行为就像调用 [Conditional] 方法,而条件的计算结果为 false。

partial class D
{
    partial void M(string message);

    void Example()
    {
        M(GetIt()); // Call to M and GetIt erased at compile time
    }

    string GetIt() => "Hello World";
}

此功能的初衷是为了通过设计器生成的代码来生成源码。 用户不断编辑生成的代码,因为他们想要调整或集成生成代码的某些方面。 最值得注意的是,Windows 窗体启动过程的一部分,在组件初始化之后。

编辑生成的代码很容易出错,因为任何导致设计器重新生成代码的操作都会导致用户编辑的内容被擦除。 partial 方法功能缓解了这种紧张,因为它允许设计人员以 partial 方法的形式生成挂钩。

设计人员可以发出像 partial void OnComponentInit() 这样的挂钩,开发人员可以为它们定义声明或不定义它们。 在任一情况下,生成的代码都可以被编译,对该过程感兴趣的开发人员可以根据需要参与其中。

这确实意味着部分方法有几个限制:

  1. 方法有 void 返回类型。
  2. 参数不能小于 out 个。
  3. 不能有任何可访问性(隐式 private)。

之所以存在这些限制,是因为当调用站点被擦除时,语言必须能够输出代码。 考虑到它们可以被擦除,private 是唯一可能的可访问性,因为成员不能在程序集元数据中公开。 这些限制也限制了 partial 方法的一系列应用场景。

这里的建议是取消围绕 partial 方法的所有现有限制。 本质上,让它们具有 out 参数、非 void 返回类型或任何类型的可访问性。 这样,partial 声明就有了定义必须存在的附加要求。 这意味着语言不必考虑移除调用点的影响。

这将扩大 partial 方法可参与的生成器应用场景,从而与我们的源生成器功能很好地结合起来。 例如,可使用以下模式定义 regex:

[RegexGenerated("(dog|cat|fish)")]
partial bool IsPetMatch(string input);

这既为开发人员提供了一种简单的声明方式来选择生成器,也为生成器提供了一组非常简单的声明,以便在源代码中查看以驱动生成的输出。

将这与生成器连接以下代码片段时的难度进行比较。

var regex = new RegularExpression("(dog|cat|fish)");
if (regex.IsMatch(someInput))
{

}

鉴于编译器不允许生成器修改与此模式相关的代码,对于生成器而言,要使用此模式几乎是不可能的。 他们需要在 IsMatch 实现中求助于反射,或者要求用户将其调用站点更改为新方法 + 重构正则表达式以将字符串字面量作为参数传递。 它太乱了。

详细设计

语言将进行更改,以允许使用显式访问修饰符对 partial 方法进行标注。 这意味着它们可以标记为 privatepublic 等...

partial 方法具有显式访问权限修饰符时,即使访问权限是 private,语言也要求声明必须有匹配的定义:

partial class C
{
    // Okay because no definition is required here
    partial void M1();

    // Okay because M2 has a definition
    private partial void M2();

    // Error: partial method M3 must have a definition
    private partial void M3();
}

partial class C
{
    private partial void M2() { }
}

此外,该语言将删除对具有显式访问权限的 partial 方法上可出现内容的所有限制。 此类声明可以包含非 void 返回类型、out 参数、extern 修饰符等...这些签名将具有 C# 语言的全部表达能力。

partial class D
{
    // Okay
    internal partial bool TryParse(string s, out int i); 
}

partial class D
{
    internal partial bool TryParse(string s, out int i) { ... }
}

这明确允许 partial 方法参与到 overridesinterface 的实现中:

interface IStudent
{
    string GetName();
}

partial class C : IStudent
{
    public virtual partial string GetName(); 
}

partial class C
{
    public virtual partial string GetName() => "Jarde";
}

partial 方法包含非法元素时,编译器将更改其发出的错误信息,本质上表述为:

不能在缺乏显式可访问性的 ref 方法上使用 partial

这将有助于开发人员在使用该功能时找到正确的方向。

限制:

  • 具有显式访问权限的 partial 声明必须具有定义
  • partial 声明和定义签名必须在所有方法和参数修饰符上匹配。 唯一可能不同的方面是参数名和属性列表(这不是新要求,而是 partial 方法的现有要求)。

问题

部分适用于所有成员

鉴于我们正在扩展 partial 以更好地支持源代码生成器,我们是否也应该扩展它以适用于所有类成员? 例如,我们是否应该能够声明 partial 构造函数、运算符等...

决议 这个想法是合理的,但在 C# 9 计划的这一点上,我们试图避免不必要的功能蔓延。 希望解决当下的问题,扩展该功能,以便与现代源代码生成器配合使用。

将考虑在 C# 10 版本中扩展 partial 以支持其他成员。 看来我们很可能会考虑此扩展。 这仍是一项积极的提议,但尚未付诸实施。

使用摘要而不是部分

该提议的核心是确保声明有相应的定义/实现。 考虑到 abstract 已经是一个语言关键字,我们是否应该使用它来促使开发人员思考实现方式?

决议 对此进行了有益的讨论,但最终还是被否决了。 是的,要求很熟悉,但概念却大不相同。 很容易误导开发人员以为他们在创建虚拟插槽,而实际上并非如此。