NDepend

Improve your .NET code quality with NDepend

C# 11 static abstract members

CSharp 11 static abstract members

C# 11 proposed interface members declared as static abstract. This is useful to handle both:

  • Polymorphism at the type level, for example when abstracting the concept of zero accross numeric types double.Zeroint.Zero this leads to T.Zero.
  • Polymorphism on operator overloading: double + doubleint + int are abstracted with T + T.

This new feature was driven by the .NET 7.0 Generic Math that we will detail later in this article. But first let’s start with an example:

Abstracting the Average Operation

Here is a simple implementation of the average operation on double:

The average operation can be abstracted as long as we can abstract the operations add and divide and the concepts of zero and one. This is now possible with static abstract members.

To compile and run this code you need to modify the .csproj file with at least .NET version 7.0 <TargetFramework>net7.0</TargetFramework>

Here are a few remarks about this code sample:

  • It would have been cleaner with only the keyword static instead of static abstract. But since C# 8 it is possible to define static members with a body within interfaces. Thus the keyword abstract is required to not mess up with this existing feature.
  • The generic constraint IMeasurable<T> where T : IMeasurable<T> looks awkward. It is required to let specify static abstract operators within the interface IMeasurable<T>. Without this constraint the compiler emits an error. This is because most operators require the types of their arguments to be of the containing type. Thus arguments of operators declared within IMeasurable<T> must be of type IMeasurable<T>.

CSharp Generic Contraint for Operator

  • Height is a record struct. This is by no mean a requirement of static abstract. However value object types defined with struct is the right way to implement lightweight numerical types like int, double and Height. Also record struct introduced in C# 10 avoids a lot of boilerplate code (constructor, properties, value-based equality, deconstruction…) as shown by the record struct vs. struct comparison below.

CSharp record struct

static abstract cannot be made virtual

Here is another code sample that harnesses static abstract.

  • The interface IVal defines both an InstanceVal and a StaticVal property.
  • Both Class1 and Class2 implement IVal .
  • Class2 derives from Class1 .
  • Class2.StaticVal hides Class1.StaticVal.

Here is the result of running this program:

At runtime the JIT builds the closed generic method ConsoleWriteLine<Class1>(Class1) to handle both calls ConsoleWriteLine(obj1) and ConsoleWriteLine(obj3). This is because both references obj1 and obj3 are typed with Class1. Thus in the context of ConsoleWriteLine<Class1>(...), the call to T.StaticVal translate to Class1.StaticVal.

static abstract members cannot be made virtual. This makes sense because virtual/override really means: take account of the type of the object at runtime to dispatch the call  to the best suited implementation. But with the keyword static there is no object involved, only type.

Here is the code of ConsoleWriteLine<T>(T val) where T : IVal decompiled to IL. We can see the usage of call IVal::get_StaticVal() and callvirt IVal::get_InstanceVal(). The JIT might have been updated to bind call IVal::get_StaticVal() with the implementation Class1::get_StaticVal() at runtime (ECMA discussion here). However this tweet from Miguel de Icaza suggests that the runtime hasn’t been updated, so I am not sure on this point.

static abstract and the Generic Math Library

Clearly the static abstract feature was driven by the upcoming Generic Math library actually in preview in .NET 7. With .NET 7, System.Runtime.dll introduces a number of new interfaces to abstract usual math operations. The new interface INumber<Self> defines what a number is and its operations:

All the good old number implementations now implement the interface INumber<Self> as shown by this code query executed against the .NET 7 compiled System.Runtime.dll assembly:

INumber implementations

This is quite a welcomed addition to .NET and C#! For example we can now have a generic implementation of the exponential formula…

Exponential MacLaurin

… and make this implementation works with double, float, Complex<T> where T: IFloatingPoint<T> or Matrix<T> where T: IFloatingPoint<T>. And yes, e raised to the power of a matrix makes send as explained in this video. This is pretty cool and this is why the original code name for static abstract and generic math was PartyDonk.

Partydonk

And last but not least, thanks to all the past efforts done on the runtime about making value-objects work seamlessly with generics (like avoiding boxing in most situations…) existing numerical types improved with generic math don’t suffer from any performance penalty.

static abstract: Not Just for Math

This new addition to C# and .NET will change the life of math-oriented .NET developers. One great use-case of static abstract is Generic Parsing and Generic Factory  explained in this post: The new .NET 7.0 IParsable<TSelf> interface

Certainly static abstract will be useful in various scenarios beyond math, numerical, parsing and factory. Indeed, this new feature let’s specify static only interfaces to parametrize some generic methods without the need to instantiate any object:

Notice that in the code above ImplA and ImplB cannot be made static although they are not meant to be instantiated. Could this point evolves until C# 11 RTM?

 

In this blog post Khalid Abuhakmeh relies on this facility to simplify the overall ASP.NET Core pipeline by defining handlers as a bunch of static abstract methods declared within an IHandler interface.

Conclusion

Again the .NET platform improves and the .NET team fills a gap. One can argue that C# is becoming overly complex year after year – which is true – but new features like static abstract and generic math do really improve concerned code. And we can agree that improving math related code is not just a niche scenario.

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly and also did manage some academic and professional courses on the platform and C#.

Over my consulting years I built an expertise about the architecture, the evolution and the maintenance challenges of large & complex real-world applications. It seemed like the spaghetti & entangled monolithic legacy concerned every sufficiently large team. As a consequence, I got interested in static code analysis and started the project NDepend in 2004.

Nowadays NDepend is a full-fledged Independent Software Vendor (ISV). With more than 12.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and full control on their application to a wide range of professional users around the world.

I live with my wife and our twin kids Léna and Paul in the beautiful island of Mauritius in the Indian Ocean.