แก้ไข

แชร์ผ่าน


Extending Partial Methods

Note

This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.

There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.

You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.

Summary

This proposal aims to remove all restrictions around the signatures of partial methods in C#. The goal being to expand the set of scenarios in which these methods can work with source generators as well as being a more general declaration form for C# methods.

See also the original partial methods specification (§14.6.9).

Motivation

C# has limited support for developers splitting methods into declarations and definitions / implementations.

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

One behavior of partial methods is that when the definition is absent then the language will simply erase any calls to the partial method. Essentially it behaves like a call to a [Conditional] method where the condition was evaluated to 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";
}

The original motivation for this feature was source generation in the form of designer generated code. Users were constantly editing the generated code because they wanted to hook some aspect of the generated code. Most notably parts of the Windows Forms startup process, after components were initialized.

Editing the generated code was error prone because any action which caused the designer to regenerate the code would cause the user edit to be erased. The partial method feature eased this tension because it allowed designers to emit hooks in the form of partial methods.

Designers could emit hooks like partial void OnComponentInit() and developers could define declarations for them or not define them. In either case though the generated code would compile and developers who were interested in the process could hook in as needed.

This does mean that partial methods have several restrictions:

  1. Must have a void return type.
  2. Cannot have out parameters.
  3. Cannot have any accessibility (implicitly private).

These restrictions exist because the language must be able to emit code when the call site is erased. Given they can be erased private is the only possible accessibility because the member can't be exposed in assembly metadata. These restrictions also serve to limit the set of scenarios in which partial methods can be applied.

The proposal here is to remove all of the existing restrictions around partial methods. Essentially let them have out, non-void return types or any type of accessibility. Such partial declarations would then have the added requirement that a definition must exist. That means the language does not have to consider the impact of erasing the call sites.

This would expand the set of generator scenarios that partial methods could participate in and hence link in nicely with our source generators feature. For example a regex could be defined using the following pattern:

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

This gives both the developer a simple declarative way of opting into generators as well as giving generators a very easy set of declarations to look through in the source code to drive their generated output.

Compare that with the difficulty that a generator would have hooking up the following snippet of code.

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

}

Given that the compiler doesn't allow generators to modify code hooking up this pattern would be pretty much impossible for generators. They would need to resort to reflection in the IsMatch implementation, or asking users to change their call sites to a new method + refactor the regex to pass the string literal as an argument. It's pretty messy.

Detailed Design

The language will change to allow partial methods to be annotated with an explicit accessibility modifier. This means they can be labeled as private, public, etc ...

When a partial method has an explicit accessibility modifier though the language will require that the declaration has a matching definition even when the accessibility is 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() { }
}

Further the language will remove all restrictions on what can appear on a partial method which has an explicit accessibility. Such declarations can contain non-void return types, out parameters, extern modifier, etc ... These signatures will have the full expressivity of the C# language.

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) { }
}

This explicitly allows for partial methods to participate in overrides and interface implementations:

interface IStudent
{
    string GetName();
}

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

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

The compiler will change the error it emits when a partial method contains an illegal element to essentially say:

Cannot use ref on a partial method that lacks explicit accessibility

This will help point developers in the right direction when using this feature.

Restrictions:

  • partial declarations with explicit accessibility must have a definition
  • partial declarations and definition signatures must match on all method and parameter modifiers. The only aspects which can differ are parameter names and attribute lists (this is not new but rather an existing requirement of partial methods).

Questions

partial on all members

Given that we're expanding partial to be more friendly to source generators should we also expand it to work on all class members? For example should we be able to declare partial constructors, operators, etc ...

Resolution The idea is sound but at this point in the C# 9 schedule we're trying to avoid unnecessary feature creep. Want to solve the immediate problem of expanding the feature to work with modern source generators.

Extending partial to support other members will be considered for the C# 10 release. Seems likely that we will consider this extension.

Use abstract instead of partial

The crux of this proposal is essentially ensuring that a declaration has a corresponding definition / implementation. Given that should we use abstract since it's already a language keyword that forces the developer to think about having an implementation?

Resolution There was a healthy discussion about this but eventually it was decided against. Yes the requirements are familiar but the concepts are significantly different. Could easily lead the developer to believe they were creating virtual slots when they were not doing so.