Static Abstract in Practice
09/16/2023
5 minutes
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.