dotnet / csharplang

The official repo for the design of the C# programming language
11.49k stars 1.02k forks source link

[API Proposal]: Simplifying Generic Extension Method Return with same Keyword #8422

Closed Juulsn closed 1 month ago

Juulsn commented 1 month ago

Background and motivation

I am using generics a lot and I'm a huge fan of the builder pattern. But one of the most frustrating things about generics and fluent APIs is, that you need to reserve a generic type for the return type.

In fluent APIs, especially those involving builders or chainable methods, it is common to work with generic extension methods. However, when multiple generic type parameters are involved, developers are often required to explicitly specify the same type multiple times, leading to verbose and repetitive code. This can reduce readability and increase the chance of errors in complex APIs. While workarounds like helper classes or verbose method signatures exist, they introduce unnecessary boilerplate. A solution to allow the return type of extension methods to automatically match the type of the calling instance, simplifying method signatures, improving type inference, and enhancing developer experience in fluent APIs is therefore needed.

This is why I'm proposing a new way to specify the return type in Extension Methods.


Currently, creating extension methods in C# for builders and fluent APIs can require verbose syntax, especially when working with generics. While it is possible to achieve the desired behavior with existing language features, the code can become unnecessarily cluttered when dealing with:

  1. Generic type arguments that cannot always be inferred.
  2. Boilerplate code to work around inference limitations (e.g., creating Helper classes or explicitly specifying multiple type arguments).
  3. Difficulty in maintaining clean, readable code when working with fluent interfaces, particularly when type arguments are involved.

Here are three current approaches, each with their respective downsides:

Approach 0a: Basic Extension Method (No Generics)

public static TSelf DoSomething<TSelf>(this TSelf builder) 
    where TSelf : ICommon
{
    // some logic
    return builder;
}

Approach 0b: Basic Extension Method (return the base type)

public static ICommon DoSomething<TSomeType>(this ICommon builder) 
        where TSomeType : notnull
    {
        // some logic

        return builder;
    }

Approach 1: Explicit Type Arguments

public static TSelf DoSomething<TSelf, TSomeType>(this TSelf builder) 
    where TSelf : ICommon
    where TSomeType : notnull
{
    // some logic
    return builder;
}

Approach 2: Boilerplate with Helper Classes

public static TypeHelper<TSelf> Helper<TSelf>(this TSelf builder) 
    where TSelf : ICommon 
{
    return new TypeHelper<TSelf>(builder);
}

public class TypeHelper<TSelf>(TSelf instance) where TSelf : ICommon
{
    public TSelf Instance => instance;

    public TypeHelper<TSelf> DoSomething<TSomeType>() where TSomeType : notnull
    {
        // some logic
        return this;
    }
}

Usage:

SomeSpecificType someSpecificType = Builder.Create().Add<SomeSpecificType>();

SomeSpecificType returnedInstance = someSpecificType
    .Helper().DoSomething<Example>().Instance;

API Proposal

Proposal: The same Keyword

To simplify and enhance the readability of generic extension methods, I propose introducing a new same keyword. The same keyword would refer to the calling object’s type (the type of this), eliminating the need to specify type arguments explicitly or introduce boilerplate code.

Example of Proposed Syntax:

public static class Extensions
{
    public static same DoSomething<TSomeType>(this same builder) 
        where same : ICommon
        where TSomeType : notnull
    {
        // some logic

        return builder;
    }
}

Extension Methods without the need of another generic argument - except the input type - might also benefit from the simpler syntax.

public static class Extensions
{
    public static same DoSomething(this same builder) where same : ICommon => builder;

    public static TSame DoSomething<TSame>(this TSame builder) where TSame : ICommon => builder;
}

API Usage

Usage Example:

SomeSpecificType someSpecificType = Builder.Create().Add<SomeSpecificType>();

SomeSpecificType returnedInstance = someSpecificType.DoSomething<Example>();

Advantages:

  1. Cleaner Syntax: Reduces the need for verbose type arguments.
  2. Improved Type Inference: Removes the need to specify the type of the calling object in most cases.
  3. Reduced Boilerplate: Removes the need for workaround constructs like TypeHelper classes.
  4. Fluent APIs: Enhances the readability and usability of fluent interfaces, particularly in builder patterns.

Alternative Designs

There are multiple alternatives to this proposal.

  1. Allow the use of Generics in the static class where the Extension Method lives
public static class Extensions<T> where T : ICommon
{
    public static T DoSomething<TSomeType>(this T builder)
        where TSomeType : notnull
    {
        // some logic

        return builder;
    }
}
  1. Advanced Type detection from usage

This might look like the following.

SomeSpecificType returnedInstance = someSpecificType.DoSomething<*, Example>();

or

SomeSpecificType returnedInstance = someSpecificType.DoSomething<, Example>();
  1. Helper Classes. As shown above, helper classes can be used to achieve similar functionality, but they introduce significant boilerplate, which can negatively impact code clarity and maintainability.
  2. Explicit Type Arguments. While functional, requiring explicit type arguments reduces the elegance and simplicity of fluent interfaces.

Risks

As far as I understand, this feature could be implemented as syntactic sugar, since there is an alternative way of doing this, with some boilerplate code.

The introduction of same would have minimal or no breaking changes at all, as it would represent a new keyword that’s only applicable in the context of extension methods. Existing code should not be affected unless same is already used as an identifier for a generic type in an extension method, in which case renaming the generic type would resolve conflicts. An alternativ could be, that a generic type with the name same has a higher priority than the proposed keyword.

Introducing another new keyword may complicate the language and make it harder to learn it. The same keyword primarily benefits scenarios where extension methods and fluent interfaces are used. It may not provide much utility beyond this specific use case. However, the keyword might be used in

dotnet-policy-service[bot] commented 1 month ago

Tagging subscribers to this area: @dotnet/area-system-reflection See info in area-owners.md if you want to be subscribed.

jkotas commented 1 month ago

This is new C# language feature proposal. New C# language feature proposals should be opened in csharplang repo. I am going to transfer it there.

Juulsn commented 1 month ago

@jkotas Ups, sorry!