Static Abstract in Practice

I recently encountered a problem where a class used a static method, and I had to provide a second implementation for this static method from one of the callsites. It looked like this case:

public class MyClass
{
    public void Foo(int input)
    {
        var value = MyStaticType.Calculate(input);
        // ...
    }
}

I needed to have a different implementation for Calculate in one place I have been invoking the Foo method. There are multiple ways to address the issue, but I have chosen one that is only available on C# 11 and above.

Alternative Solutions

First, let me elaborate on some of the alternative solutions. There are two alternative solutions that I discuss in this post.

One alternative solution could be to provide an extra bool or enum parameter to Foo method, which is then passed to Calculate method. Calculate method then can branch implementations based on the input. While this solution is straightforward, it involves an extra branching operation and passing the parameter. Eventually, we would end up with a second implementation of the Calculate method in the static MyStaticType type. However it might not belong to the same domain of MyStaticType, hence a new class could be necessary.

Another alternative could be converting MyStaticType to a non-static type and passing it as an input parameter of the Foo method (if someone does not prefer it as a method parameter, it could also be a type dependency of MyClass). Foo would not depend on MyStaticType directly, but it would need an input of a contract that is both implemented by MyStaticType and the alternative implementation (let us name it MyAlternativeType). This solution requires to create instances of either MyStaticType or MyAlternativeType. Depending on the situation someone might want to avoid this extra allocation.

Using Static Abstract

A third solution could be abstracting the static class. In the past C# did not allow to create interfaces for static methods, but with C# 11 and static abstract methods in interfaces we can create a contract.

With this I could design a contract such as:

public interface IMyContract
{
    public static abstract int Calculate(int input);
}

At this point both MyStaticType and MyAlternativeType can implement this interface. Unfortunately, they have to drop the static constraint in their type declaration:

public class MyStaticType : IMyContract
{
    public static int Calculate(int input) => input + 1;
}

public class MyAlternativeType : IMyContract
{
    public static int Calculate(int input) => input - 1;
}

Method Foo can declare a generic type T, and constrain it to IMyContract:

public class MyClass
{
    public void Foo<T>(int input) where T : IMyContract
    {
        var value = T.Calculate(input);
        // ...

Qt the call site of Foo, one can invoke it by passing the type as a generic type argument, that provides the static Calculate method: c.Foo<MyStaticType>(1);. This way we do not need to create a new instance for either of the implementations of IMyContract interface. Neither do we have to pass an extra parameter to the method - as the compiler will take care of passing this knowledge as a generic type parameter.

Double Down Generics

One can push the idea further and double down on generics. Can we make the operation in MyClass generic too? One approach could be making the Calculate method generic, but applying any constraint on the static abstract method will be forced on the actual implementations too. However, we can make the interface generic such as:

public interface IMyContract<T>
{
    public static abstract T Calculate(T input);
}

Derived types now can be generic, and even better, they may add different type constraints on the generic parameter:

public class MyStaticType<T> : IMyContract<T> where T : IBinaryNumber<T>
{
    public static T Calculate(T input) => input + T.One;
}

public class MyAlternativeType<T> : IMyContract<T> where T : IBinaryInteger<T>
{
    public static T Calculate(T input) => input - T.One;
}

In this case MyStaticType requires that T to be an IBinaryNumber<T>, while MyAlternativeType requires it to be IBinaryInteger<T>. Please note, there is nothing specific to this implementation to require an IBinaryInteger<T>, it is purely for the sake of the example.

This allows us to generalize Foo method on the input type too:

public class MyClass<T>
{
    public void Foo<U>(T input) where U : IMyContract<T>
    {
        var value = U.Calculate(input);
        // ...

Instead of adding the generic type parameter on the method, it is again added to the class definition of MyClass. Notice Foo still has a generic type parameter U, which has a constraint to be IMyContract<T>. Notice, Foo method, has no constraints on the type T, all the constraints on it will come due to the constraints depending on the actual implementation of IMyContract.

For example, this setup allows to invoke Foo with MyStaticType or MyAlternativeType with integer type parameters, such as short or int:

var example0 = new MyClass<short>();
example0.Foo<MyAlternativeType<short>>(1);

var example1 = new MyClass<int>();
example1.Foo<MyAlternativeType<int>>(1);

var example2 = new MyClass<short>();
example2.Foo<MyStaticType<short>>(1);

var example3 = new MyClass<int>();
example3.Foo<MyStaticType<int>>(1);

While MyStaticType allows non-integer types like double, MyAlternativeType does not. As shown below the compiler enforces this constraint, and while example4 compiles, example5 will result a compiler error:

CS0315 The type 'double' cannot be used as type parameter 'T' in the generic type or method 'MyAlternativeType'. There is no boxing conversion from 'double' to 'System.Numerics.IBinaryInteger'.

var example4 = new MyClass<double>();
example4.Foo<MyStaticType<double>>(1);

var example5 = new MyClass<double>();
example5.Foo<MyAlternativeType<double>>(1); // Compiler error

Conclusion

Previously I found static abstract methods a bit impractical for line of business applications. However, this example shows that they can come in very handy when we need to abstract static methods (no pun intended), while avoiding the alternatives described above. Combining it with regular generic types and constraints, a developer may define a strong control over how types may be composed, while keeping the implementation generic.