C# 14 – Exploring extension members

C# 14 introduces extension members. C# has long had extension methods and the new extension member syntax builds on this familiar feature. The latest preview adds static extension methods and instance and static extension properties. We’ll release more member kinds in the future.

Extension members also introduce an alternate syntax for extension methods. The new syntax is optional and you do not need to change your existing extension methods. In this post, we’ll call the existing extension method syntax this-parameter extension methods and the new syntax extension members.

No matter the style, extension members add functionality to types. That’s particularly useful if you don’t have access to the type’s source code or it’s an interface. If you don’t like using !list.Any() you can create your own extension method IsEmpty(). Starting in the latest preview you can make that a property and use it just like any other property of the type:

// An extension property `IsEmpty` is declared in a separate class
public void Operate(IEnumerable<string> strings)
{
   if (strings.IsEmpty)
   {
       return;
   }
   // Do the operation
}

Using the new syntax, you can also add extensions that work like static properties and methods on the underlying type.

This post looks at the benefits of the current syntax, the design challenges we solved, and a few deeper scenarios. If you’ve never written an extension method, you’ll see how easy it is to get started.

Creating extension members

Here are examples of writing extensions using this-parameter extension syntax and the new syntax:

public static class MyExtensions
{
    public static IEnumerable<int> ValuesLessThan(this IEnumerable<int> source, int threshold)
            => source.Where(x => x < threshold);

    extension(IEnumerable<int> source)
    {
        public IEnumerable<int> ValuesGreaterThan(int threshold)
            => source.Where(x => x > threshold);

        public IEnumerable<int> ValuesGreaterThanZero
            => source.ValuesGreaterThan(0);
    }
}

Extension members need two kinds of information: the receiver that the member should be applied to, and what parameters it needs if it’s a method. This-parameter extension methods, like ValueLessThan, put these together in the parameter list, prefixing the receiver with this.

The new extension method syntax separates the receiver, placing it on the extension block. This provides a home for the receiver when the member does not have parameters, like extension properties.

Within the extension block, code looks just like it would appear if the members were placed on the underlying type.

Another benefit of the extension member syntax is grouping extensions that apply to the same receiver. The receiver’s name, type, generic type parameters, type parameter constraints, attributes and modifiers are all applied in the extension block, which avoids repetition. Multiple extension blocks can be used when these details differ.

The containing static class can hold any combination of extension blocks, this-parameter extension methods, and other kinds of static members. Even in greenfield projects that use only the new extension member syntax, you may also declare other static helper methods.

Allowing extension blocks, this-parameter extension methods, and other static members in the same static class minimizes the changes you need to make to add new kinds of extension members. If you just want to add a property, you just add an extension block to your existing static class. Since you can use the two syntax styles together, there is no need to convert your existing methods to take advantage of the new member types.

Extension blocks gives you full freedom to organize your code however it makes sense. That’s not just to support personal or team preference. Extension members solve many different kinds of problems that are best handled with different static class layouts.

As we designed extension members, we looked at code in public locations like GitHub and found huge variations in how people organize this-parameter extension methods. Common patterns include static classes that contain extension methods for a specific type (like StringExtensions), having all the extensions for a project grouped in a single static class, and grouping multiple overloads with different receiver types into a static class. These are all the right approach to certain scenarios.

The way that extensions are organized into their containing static classes is significant because the static class name is used to disambiguate. Disambiguation is uncommon, but when ambiguity occurs the user needs a solution. Moving an extension to a differently named static class is a breaking change because someone somewhere uses the static class name to disambiguate.

Design challenges for extension members

The C# design team considered supporting other member kinds multiple times since this-parameter extension methods were introduced. It’s been a hard problem because challenges are created by the underlying realities:

  • Extension methods are deeply embedded in our ecosystem – there are a lot of them and they are heavily used. Any changes to the extension syntax needs to be natural for developers consuming them.
  • Converting an extension method to a new syntax should not break code that uses it.
  • Developers organize their extensions appropriately for their scenario.
  • The receiver has to be declared somewhere – methods have parameters but properties and most other member kinds do not.
  • The current approach for disambiguation is well known and based on the containing static class name.
  • Extension methods are called much more often than they are created or altered.

The next section takes a deeper look at receivers, disambiguation, and balancing the needs of extension authors and developers that use extensions.

Receivers

When you call an extension method, the compiler rewrites the call, which is called lowering. In the code below, calling the method on the receiver (to assign x1) is lowered to calling the static method with the receiver as the first parameter, which is equivalent to the code that sets x2:

int[] list = [ 1, 3, 5, 7, 9 ];
var x1 = list.ValuesLessThan(3);
var x2 = MyExtensions.ValuesLessThan(list, 3);

In the declaration of a this-parameter extension method, the receiver is the first parameter and is prefixed with this:

public static class MyExtensions
{
    public static IEnumerable<int> ValuesLessThan(this IEnumerable<int> source, int threshold) ...

In this declaration, the receiver is this IEnumerable<int> source and int threshold is a parameter to the method. This works well for methods, but properties and most other kinds of members do not have parameters that can hold the receiver. Extension members solve this by placing the receiver on the extension block. The ValueLessThan method in the new syntax would be:

public static class MyExtensions
{
    extension(IEnumerable<int> source)
    {
        public IEnumerable<int> ValuesLessThan(int threshold) ...

The compiler lowers this to a static method that is identical to the previous this-parameter extension method declaration, using the receiver parameter from the extension block. The lowered version is available to you to disambiguate when you encounter ambiguous signatures. This approach also guarantees that extension methods written with the this-parameter syntax or the new extension member syntax work the same way at both source and binary levels.

Properties lower to get and set methods. Consider this extension property:

public static class MyExtensions
{
   extension(IEnumerable<int> source)
    {
        public IEnumerable<int> ValuesGreaterThanZero
        {
             get
             { ...

This lowers to a get method with the receiver type as the only parameter:

public static class MyExtensions
{
    public static IEnumerable<int> get_ValuesGreaterThanZero(this IEnumerable<int> source) ...    

For both methods and properties, the type, name, generic type parameters, type parameter constraints, attributes and modifiers of the extension block are all copied to the receiver parameter.

Disambiguation

Generally, extensions are accessed as members on the receiver just like any other property or method, such as list.ValuesGreaterThan(3). This simple approach works well almost all the time. Occasionally, an ambiguity arises because multiple extensions with valid signatures are available. Ambiguities are rare, but when they occur the user needs a way to resolve it.

Disambiguation for this-parameter extension methods is done by calling the static extension method directly. Disambiguation for the new extension member syntax is done by calling the lowered methods created by the compiler:

var list = new List<int> { 1, 2, 3, 4, 5 };
var x1 = MyExtensions.ValuesLessThan(list, 3);
var x2 = MyExtensions.ValuesGreaterThan(list, 3);
var x3 = MyExtensions.get_ValuesGreaterThanZero(list);

Using the containing static class is consistent with traditional extension methods and the approach we think developers expect. When the static class name is entered, IntelliSense will display the lowered members, such as get_ValuesGreaterThanZero.

Prioritizing extension users

We believe the most important thing for extension users is that they never need to think about how an extension is authored. Specifically, they should not be able to tell the difference between extension methods that use the this-parameter syntax and the new style syntax, existing extension methods continue to work, and users are not disrupted if the author updates a this-parameter extension method to the new syntax.

The C# team has explored adding other types of extension members for many years. There is no perfect syntax for this feature. While we have many goals, we settled on prioritizing first the experience of developers using extensions, then clarity and flexibility for extension authors, and then brevity in syntax for authoring extension members.

Static methods and properties

Static extension members are supported and are a little different because there isn’t a receiver. The parameter on the extension block does not need a name, and if there is one it is ignored:

public static class MyExtensions
{
    extension<T>(List<T>)
    {
        public static List<T> Create()
            => [];
    }

Generics

Just like this-parameter extension methods, extension members can have open or concrete generics, with or without constraints:

public static class MyExtensions
{
    extension<T>(IEnumerable<T> source)
        where T : IEquatable<T>
    {
        public IEnumerable<T> ValuesEqualTo(T threshold)
            => source.Where(x => x.Equals(threshold));
    }

Generic constraints allow the earlier examples to be updated to handle any numeric type that supports the INumber<T> interface:

public static class MyExtensions
{
    extension<T>(IEnumerable<T> source)
        where T : INumber<T>
    {
        public IEnumerable<T> ValuesGreaterThan(T threshold)
            => source.Where(x => x > threshold);

        public IEnumerable<T> ValuesGreaterThanZero
            => source.ValuesGreaterThan(T.Zero);

        public IEnumerable<TResult> SelectGreaterThan<TResult>(
                T threshold,
                Func<T, TResult> select)
            => source
                 .ValuesGreaterThan(x => x > threshold)
                 .Select(select);
    }

The type parameter on the extension block (<T>) is the generic type parameter inferred from the receiver.

The SelectGreaterThan method also has a generic type parameter <TResult>, which is inferred from the delegate passed to the select parameter.

Some extension methods cannot be ported

Rules for the new extension member syntax will result in a few this-parameter extension methods needing to remain in their current syntax.

The order of generic type parameters in the lowered form of the new syntax is the receiver type parameters followed by the method type parameters. For example, SelectGreaterThan above would lower to:

public static class MyExtensions
{
    public static IEnumerable<TResult> SelectGreaterThan<T, TResult>(
                this IEnumerable<T> source, T threshold, Func<T, TResult> select) ...

If the type parameters of the receiver do not appear first, the method cannot be ported to the new syntax. This is an example of a this-parameter extension method that can not be ported:

public static class MyExtensions
{
    public static IEnumerable<TResult> SelectLessThan<TResult, T>(
                this IEnumerable<T> source, T threshold, Func<T, TResult> select) ...

Also, you can’t currently port to the new syntax if a type parameter on the receiver has a constraint that depends on a type parameter from the member. We’re taking another look at whether we can remove that restriction due to feedback.

We think these scenarios will be rare. Extension members that encounter these limitations will continue to work using the this-parameter syntax.

A small problem with nomenclature

The C# design team often refers to extension members in terms of static and instance, methods and properties. We mean – methods and properties that behave as though they are either static or instance methods and properties on the underlying type. Thus, we think of this-parameter extension methods as being instance extension methods. But, this might be confusing because they are declared as static methods in a static class. Hopefully being aware of this possible confusion will help you understand the proposal, articles and presentations.

Summary

Creating extension members has been a long journey and we’ve explored many designs. Some needed the receiver repeated on every member, some impacted disambiguation, some placed restrictions on how you organized your extension members, and some created a breaking change if you updated to the new syntax. Some had complicated implementations. Some just didn’t feel like C#.

The new extension member syntax preserves the enormous body of existing this-parameter extension methods while introducing new kinds of extension members. It offers an alternate syntax for extension methods that is consistent with the new kinds of members and fully interchangeable with the this-parameter syntax.

You can find out more about extension members in the C# 14 Preview 3 Unboxing and you can check out all the new features at What’s new in C# 14.

We heard the feedback that the extra block and indentation level is surprising and wanted to share how we arrived at these design decisions.

Keep the feedback and questions coming. We love talking about C# features and can’t wait to see what you build with extension members!

The post C# 14 – Exploring extension members appeared first on .NET Blog.

Previous Article

Why we built our startup in C#

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *