NDepend Blog

Improve your .NET code quality with NDepend

C# static abstract members

May 14, 2024 5 minutes read

C# 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 across 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 which 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 or upper like <TargetFramework>net8.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 be able to use 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 means 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 Generic Math library.With .NET 7.0, System.Runtime.dll introduced 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

An implementation of this formula can work with double, float, Complex<T> where T: IFloatingPoint<T> or Matrix<T> where T: IFloatingPoint&lt;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 .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 lets us 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.

Conclusion

Once again, the .NET platform advances, and the .NET team addresses a gap. While it’s true that C# is becoming increasingly complex each year, new features like static abstract members and generic math genuinely enhance the code. Additionally, we can agree that improving math-related code is not merely a niche scenario.