Why add static abstract members?

When you have an interface declared on a class it tells you that every instance of that class has the members that are declared in the interface. In that way the compiler knows what members you can use on any of the instances of the given type. Besides the members of the object you can also define static members on a class. Those are the members that will be available on the type and not on the instance of the class. Until now an interface implementation did not have anything to do with those static members. This changes with static abstract members. With this feature it will be possible to define that the type of a class that implements the interface must have certain static members. This can be very useful in the case of operators (operators in C# must be declared static). This is also the main driver for this feature. In .NET 7 Generic Math will be introduced and this is heavily dependent on static abstract members. Let’s take a look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public int AddNumbers(IEnumerable<int> numbersToAdd)
{
    int result = 0;
    foreach(var number in numbersToAdd)
    {
        result += number;
    }
    return result;
}

AddNumbers(new int[] { 1, 3, 13 });

In the example above you see a method that accepts a list of integers. All the integers in the list will be added and the result will be returned. Now imagine that you also want this method to work with other numeric types like doubles, floats and longs. My first thought would be to make the method generic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
T AddNumbersGeneric<T>(IEnumerable<T> numbersToAdd) 
{
    T result = default;
    foreach (var number in numbersToAdd)
    {
        result += number;  // compiler error CS0019: Operator '+=' cannot be applied to operands of type 'T' and 'T'
    }
    return result;
}
Console.WriteLine(AddNumbersGeneric(new long[] { 1, 3, 13 }));

Now the parameter is generic and I initialized the result variable to the default of it’s type. The example above however won’t compile because the compiler doesn’t know if the type T has a + operator implemented. This is now the only thing blocking the generic implementation. So if you want to realize this in the current C# version you have to make an own implementation of AddNumbers for every numeric type. This would add a lot of the same code with only the type being different each time. Generic Math in .NET 7 solves this by relying on static abstract members.

Generic math in .NET 7 and the use of static abstract members

In .NET 7 a new interface INumber is added. Let’s look at the interface definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 public interface INumber<TSelf> : IComparable, IComparable<TSelf>, 
 IEquatable<TSelf>, IFormattable, IParsable<TSelf>, ISpanFormattable, 
 ISpanParsable<TSelf>, IAdditionOperators<TSelf, TSelf, TSelf>, 
 IAdditiveIdentity<TSelf, TSelf>, IComparisonOperators<TSelf, TSelf>, 
 IEqualityOperators<TSelf, TSelf>, IDecrementOperators<TSelf>, 
 IDivisionOperators<TSelf, TSelf, TSelf>, IIncrementOperators<TSelf>, 
 IModulusOperators<TSelf, TSelf, TSelf>, IMultiplicativeIdentity<TSelf, TSelf>,
 IMultiplyOperators<TSelf, TSelf, TSelf>, INumberBase<TSelf>,
 ISubtractionOperators<TSelf, TSelf, TSelf>, 
 IUnaryNegationOperators<TSelf, TSelf>, IUnaryPlusOperators<TSelf, TSelf> where TSelf : INumber<TSelf>
    {
        static abstract TSelf Abs(TSelf value);
        static abstract TSelf Clamp(TSelf value, TSelf min, TSelf max);
        static abstract TSelf CopySign(TSelf value, TSelf sign);
        static abstract TSelf CreateChecked<TOther>(TOther value) where TOther : INumber<TOther>;
        static abstract TSelf CreateSaturating<TOther>(TOther value) where TOther : INumber<TOther>;
        static abstract TSelf CreateTruncating<TOther>(TOther value) where TOther : INumber<TOther>;
        static abstract bool IsNegative(TSelf value);
        static abstract TSelf Max(TSelf x, TSelf y);
        static abstract TSelf MaxMagnitude(TSelf x, TSelf y);
        static abstract TSelf Min(TSelf x, TSelf y);
        static abstract TSelf MinMagnitude(TSelf x, TSelf y);
        static abstract TSelf Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider);
        static abstract TSelf Parse(string s, NumberStyles style, IFormatProvider? provider);
        static abstract int Sign(TSelf value);
        static abstract bool TryCreate<TOther>(TOther value, out TSelf result) where TOther : INumber<TOther>;
        static abstract bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out TSelf result);
        static abstract bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out TSelf result);
    }

As you can see it has a lot of static abstract members that all the numeric types in .NET implement. Next to that it also inherits from a lot of other new interfaces that all declare static abstract members that are implemented by the different numeric .NET types. Take the IUnaryPlusOperators<TSelf, TSelf> interface for example. This interface adds the + operator. It is declared on a separate interface so that you can also reuse that for other types that implement a + operator. The IUnaryPlusOperators<TSelf, TSelf> looks like this:

1
2
3
4
public interface IUnaryPlusOperators<TSelf, TResult> where TSelf : IUnaryPlusOperators<TSelf, TResult>
{
    static abstract TResult operator +(TSelf value);
}

With all those interfaces added a new and straightforward implementation of our AddNumbers method becomes available:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
T AddNumbers<T>(IEnumerable<T> numbersToAdd) where T: INumber<T>
{
    T result = default;
    foreach (var number in numbersToAdd)
    {
        result += number;  // this now compiles because T implements INumber<T>
    }
    return result;
}
Console.WriteLine(AddNumbers(new long[] { 1, 3, 13 }));
Console.WriteLine(AddNumbers(new double[] { 1.0, 3.0, 13.12 }));
Console.WriteLine(AddNumbers(new short[] { 1, 2, 3 }));

All the method calls to the AddNumbers method will work now. Next to the INumber interface a lot of other interfaces using this are added. Another example is IParsable. This interface defines 2 methods that a type must have:

1
2
3
4
5
public interface IParsable<TSelf> where TSelf : IParsable<TSelf>
{
    static abstract TSelf Parse(string s, IFormatProvider? provider);
    static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TSelf result);
}

Using the IParsable interface makes it possible to have 2 different types handled in the same way as long as they implement the static methods Parse and TryParse.

The same also works for properties. For example in .NET all numeric types have static properties One an Zero. Those properties can be accessed and used on the type like any normal static member. For example I could change the previous example. In that example we initialized the result to the default of the given type. In the next example the result will be initialized with the Zero property of the generic (INumber<T>)type.

1
2
3
4
5
6
7
8
9
T AddNumbers<T>(IEnumerable<T> numbersToAdd) where T: INumber<T>
{
    T result = T.Zero; // initialized with the Zero value of the generic type
    foreach (var number in numbersToAdd)
    {
        result += number; 
    }
    return result;
}

Just one more example. Next to the INumber<TSelf> there is also an interface IMinMaxValue<T>. This interface exposes the static members MinValue and MaxValue that almost all numeric types have. It is in a separate interface because not all numeric types implement this (BigInteger for example does not implement this). The following example shows how the MaxValue of different types is written to the console using one generic method.

1
2
3
4
5
6
7
8
void ShowMaxValue<T>() where T: IMinMaxValue<T>
{
    Console.WriteLine(T.MaxValue);
}
ShowMaxValue<int>();
ShowMaxValue<long>();
ShowMaxValue<decimal>();
ShowMaxValue<float>();

Why not just static members?

You could ask why is it needed to make a member static and abstract? My first thought was that static would be enough since you want to tell that the member should not be on the object instance of the class but be a static member on the type. The fact that it is not implemented this way is because in C# 8.0 another feature Static interface members was added to the language. This feature makes it possible to add method static implementations to your interface (not the best addition to the language in my opinion). You can for example define the following interface:

1
2
3
4
5
6
7
8
9
interface ICSharp8Example
{
    void DoSomething();
    static void DoAnotherThing()
    {
        Console.WriteLine("Hello from a static method on an interface.");
    }
}
ICSharp8Example.DoAnotherThing();

With this feature static members where already taken on interfaces. That is why this feature uses static abstract to indicate that the member should be implemented on the type.

How can it be useful in your own code / types?

Generic Math is in my opinion a very good use case where static abstract members can be used. This is mainly because operators in C# are always static and because all numeric types share the fact that they have things like a Zero, a One and stuff like calculating an average. Those things are static for a reason. They could have been added as instance properties / methods but that would be a waste of memory resources since stuff like Zero and One are the same for all instances. So this is a perfect example where it is handy that an interface defines that a type should have certain members where the compiler can rely on.

Having said that I don’t have a good example for my own code where I would use this feature now. But when you have a scenario with static members shared across types and you need to handle them in the same way this is a way to make your code cleaner (just like Microsoft did with generic math). If you have a good scenario for this let me know in the comments.

Conclusion

Looking at the generic math implementation using static abstract members I think it has already proven to be a very useful language feature. The addition of interfaces like INumber<T> (but also stuff like IParsable) are very useful and makes your code cleaner. Next to that you can also benefit from it in your own code when you have a scenario with static members on different types that have the same functional meaning.

References

comments powered by Disqus