C#10 was just released, but included in this years release is an early preview of C#11, and it contains quite an exciting change: static abstract members in interfaces. You might think, what’s the fuzz? Static members in interfaces is nothing new and was introduced in C#8! In this post I present a few more fruitful, but perhaps less useful, examples of this new feature.
The fuzz is, in C#11 you can now specify, operator operator implementations and implicit conversions, to name just a few, but also plain simple static methods. As a simple example, you can now require a simple factory method for creating parameterized instances of any implementations of an interface: “every type implementing this interface must have a static member of the form…”. For factory methods, think of the new() generic type constrain, but with parameters.
Self-referencing generic types
Before diving into this new exciting feature, we take a look at self-referencing generic types, also known as the curiously recurring template pattern. This patterns will be used frequently the motivating examples. The following example is preC#10, so nothing new here; it is however important. Previously, you could sort of require parameterless constructors by interface implementations. We could define an INewable interface like this:
interface INewable<T> where T : INewable<T>,new() {
T Self{get;}
}
We added a T Self{get;}
not because it is particular useful for INewables
, but because it helps clarify my the example. Implementations of the above interface must provide at least a public default constructor, possibly implicit and the property Self
, in this case, returning this
:
class Foo : INewable<Foo>{
public Foo Self => this;
}
Foo above cannot have only the string parameterized constructor Foo(string s)
. Adding such would cause the code not to compile; however, having both Foo(string)
and Foo()
would be allowed.
The above is nothing new, but the self-referencing generic type pattern is used when we want to future reference the implementing type in the interface. In the above case, Foo must have a Self of type Foo. Although this is not true, as there are alternative implementations violating this:
class Bar : INewable<Foo>{
public Foo Self{get;}
}
Bar now have no constructor restrictions; however, the restrictions apply to the usage of Foo
as T in INewable<T>,
with the expected requirement. Bottom line: it does not apply, which may be what we wanted, for some reason.
In the official repo for the design of the C# programming language, a Self constraint for generic type parameters has been proposed, which would introduce concise syntax for the above and better communicate intention. It could also make room for further refinement of the concept.
Simple example: a csv-parse factory method
The simplest example of static abstracts would be a static factory method. If we want implementers to provide a parse method for example, we could create an ICsvParsable-interface and let for example a Person class implement it:
This would allow us to provide a generic parser method:
IEnumerable<T> ParseCsvData<T>(string data) where T : ICsvParseable<T>
{
return data.Split('\n').Skip(1).Select(T.FromCsv);
}
If something is ICsvParseable, this method can turn in a sequence of lines into a sequence of objects. Key getaway here is, we can create complex objects by referring only to their least abstractions, ICsvParsable<T>.
Abstract operators and their implementations
Surely, if factory methods was all, we could come up with a reasonable alternative to the above definition. But we can do way more than the above. Consider the interface IBasicOps<T> below:
This interface specifies that the operators + and – must be defined for implementing types along with the constant values zero and one; additive and multiplicative identities. Operators in interfaces, allows us to naturally express generic math. Using IBasicOps<T>, we can for example express a Fibonacci function for every type implementing IBasicOps<T>:
And for our purpose, a simple basic implementation of a number, N:
In itself nothing fancy, but the fib function would also work for, say a Vector : IBasicOps<Vector>
. We could even create a Tracing decorator, which prints out operations performed on any IBasicOps<T>:
Calculating the 4th Fibonacci number using the Tracing decorator as in the example code:
N v = ml.Fibonacci(new Tracing<N>(new N(4))).Value;
Console.WriteLine($"result: {v}");
gives the output:
Or a LazyNumber implementation, for which calculations are only performed if you actually need the result:
As these are decorations of any IBasicOps<T>, these can be combined we can do calculations using lazy tracing numbers where the code
var v = new LazyNumber>(new Tracing(new N(4)));
var u = v + v + v + v;
would print nothing but computed: 4 = 4
, because we are initializing the number, whereas the code where we actually care about the result:
var v = new LazyNumber>(new Tracing(new N(4)));
var u = v + v + v + v;
Console.WriteLine(u.Value.Value.Value); // don't mind the values
would print the result of the computation but also all computations involved:
computed: 4 = 4
computed: 4+4 = 8
computed: 4+4+4 = 12
computed: 4+4+4+4 = 16
16
… but you should probably ask yourself, if you really want to.
Core interfaces in the .NET standard library
We fortunately do not have to build interfaces like IBaseOps<T> ourselves, as the standard library already contain operator oriented interfaces and much more. Not only did they add a bunch of interfaces, but types like int, float and friends, implement the expected interfaces as should be expected! This means that the above decorations would work on for example integers just out of the box! Below are just a snippet of the newly added interfaces; the interface for Addition and the expected interface of numbers in INumber<T>:
interface IAdditionOperators<TSelf, TOther, TResult>
where TSelf : IAdditionOperators<TSelf, TOther, TResult>
{
static TResult operator +(TSelf left, TOther right);
}
interface INumber<TSelf> :
IAdditionOperators<TSelf, TSelf, TSelf>,
IAdditiveIdentity<TSelf, TSelf>, IComparable,
IComparable<TSelf>,
IComparisonOperators<TSelf, TSelf>,
IEqualityOperators<TSelf, TSelf>,
IEquatable<TSelf>,
IDecrementOperators<TSelf>,
IDivisionOperators<TSelf, TSelf, TSelf>,
IFormattable,
IIncrementOperators<TSelf>,
IModulusOperators<TSelf, TSelf, TSelf>,
IMultiplicativeIdentity<TSelf, TSelf>,
IMultiplyOperators<TSelf, TSelf, TSelf>,
IParseable<TSelf>,
ISpanFormattable,
ISpanParseable<TSelf>,
ISubtractionOperators<TSelf, TSelf, TSelf>,
IUnaryNegationOperators<TSelf, TSelf>,
IUnaryPlusOperators<TSelf, TSelf>
where TSelf : INumber<TSelf> { ...}
The original proposal supplies other examples, and this post, on Preview Features in .NET 6 – Generic Math from Tanner Gooding is another excellent read on this topic, with a lot more and more sane examples than I’ve presented here.