[API Proposal]: Simplifying Generic Extension Method Return with same Keyword
Juulsn opened this issue · 3 comments
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:
- Generic type arguments that cannot always be inferred.
- Boilerplate code to work around inference limitations (e.g., creating
Helper
classes or explicitly specifying multiple type arguments). - 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;
}
- Pros: Simple and effective when no additional types are required.
- Cons: Doesn't work if the extension method requires a generic type that isn’t directly related to
TSelf
.
Approach 0b: Basic Extension Method (return the base type)
public static ICommon DoSomething<TSomeType>(this ICommon builder)
where TSomeType : notnull
{
// some logic
return builder;
}
- Pros: No need to specify the return type via generics.
- Cons: Doesn't work if the extension method requires to return the initial type, which is mostly needed to chain calls.
Approach 1: Explicit Type Arguments
public static TSelf DoSomething<TSelf, TSomeType>(this TSelf builder)
where TSelf : ICommon
where TSomeType : notnull
{
// some logic
return builder;
}
- Pros: Allows for more flexibility by introducing a second generic type (
TSomeType
). - Cons: Requires explicit type arguments, even when they could theoretically be inferred from context, resulting in verbose and less readable code.
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;
- Pros: Works well and allows for flexible type usage.
- Cons: Requires significant boilerplate, reducing code readability and maintainability.
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;
}
}
- In this case,
same
represents the type of the calling object (builder
), making the method more concise and easier to understand. TSomeType
remains a generic type that must be specified, but the type of the calling object (same
) is inferred automatically.
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:
- Cleaner Syntax: Reduces the need for verbose type arguments.
- Improved Type Inference: Removes the need to specify the type of the calling object in most cases.
- Reduced Boilerplate: Removes the need for workaround constructs like
TypeHelper
classes. - Fluent APIs: Enhances the readability and usability of fluent interfaces, particularly in builder patterns.
Alternative Designs
There are multiple alternatives to this proposal.
- 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;
}
}
- Advanced Type detection from usage
This might look like the following.
SomeSpecificType returnedInstance = someSpecificType.DoSomething<*, Example>();
or
SomeSpecificType returnedInstance = someSpecificType.DoSomething<, Example>();
- 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.
- 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
Tagging subscribers to this area: @dotnet/area-system-reflection
See info in area-owners.md if you want to be subscribed.
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.