Static Abstract Interface Members In C# 11 And Curiously Recurring Template Pattern

C# 11 introduces us to a lot of improvements in the language. Out of the many key improvements, this article focuses on a hidden gem which, in fact, was one of the key foundation stone, upon which some of the key Math generic functionalities were built on - the Static Abstract Interface Members.

Way back in C# 8, the language enabled us to write static members for interfaces, which enabled us to execute code from an interface without the need for instantiating an instance. In C# 11, we take this further by introducing Static Abstract Members in interface.

Let us define an interface with static abstract members.

public interface IBird {
    static abstract bool CanFly();
    static abstract IBird Clone();
}

We have defined an interface IBird with a single method CanFly(), which is defined as static abstract. Now the implementors can provide a more specific implementation of the static method For example,

public class Parrot: IBird {
    public static bool CanFly() => true;
}
public class Kiwi: IBird {
    public static bool CanFly() => false;
}

Each of the types Parrot and Kiwi now exposes a static member, CanFly(), with specific implementation of its own. Remember these are static members and you do not need to instantiate an instance for using them in your client code.

Console.WriteLine("Demo code for exploring Static Virtual Members in Interface");
Console.WriteLine($"{nameof(Parrot)}: Can Fly - {Parrot.CanFly()}");
Console.WriteLine($"{nameof(Kiwi)}: Can Fly - {Kiwi.CanFly()}");

The significance of the feature is magnified as it acts as the enabler for implementing operator overloading for types implementing the interface. We will get back to it in a bit. Before that, we want to explore another important concept.

Curiously Recurring Template Pattern

Consider the following interface IFooBar

internal interface IFruit
{
    static abstract IFruit CreateInstance();
}

The interface defines a static abstract memberCreateInstance that returns a type of IFruit. An implementation of the interface could be as follows.

internal class Apple : IFruit
{
    public static IFruit CreateInstance() => new Apple();

    public void SayHello() => Console.WriteLine("Hello");
}

The Apple class implements the IFruit interface. Additionally, it also implements an instance method SayHello(). If you would want to create an instance of Apple with the CreateInstance() method and then invoke the SayHello() method using the newly created instance, the code would look like the following.

var fooBar = Apple.CreateInstance();
((Apple)fooBar).SayHello();

Notice that you are forced to explicitly downcast the instance to the derived type. What if we could make the code more readable by passing the derived type as generic parameter of the interface. Let us modify the interface to accept a generic parameter.

internal interface IFruit<TDerieved>
{
    static abstract TDerieved CreateInstance();
}

internal class Apple : IFruit<Apple>
{
    public static Apple CreateInstance() => new Apple();

    public void SayHello() => Console.WriteLine("Hello");
}

We have now modified the interface to accept a generic parameter, which is used as a return type for the CreateInstance method. We can also now modify the Apple implementation to return a more specific type now. This removes the need for downcasting the instance in the consuming code.

var fooBar = Apple.CreateInstance();
fooBar.SayHello();

However, we still have a problem. There is no constraint on the TDerieved generic parameter of the interface and this could be anything. For example, consider the following code.

internal class Orange: IFruit < int > {
    public static int CreateInstance() => 0;
    public void SayHello() => Console.WriteLine("Hello");
}

The Orange class implements the interface IFruit<T>, however, instead of passing a derived type of IFruit as generic parameter, it passes int type. This is perfectly legal, but not as desired. So how would one force the generic parameter to be actually a derived type of the interface itself. This is where Curiously Recurring Template Pattern comes into play.

Let us now rewrite the interface with the pattern.

internal interface IFruit < TDerieved > where TDerieved: IFruit < TDerieved > {
    static abstract TDerieved CreateInstance();
}

We have now added a generic constraint that specifies that the generic pattern TDerieved should be an implementation of IFruit<TDerieved>. This would force the implementing classes to use a derived type of interface as generic parameter. For example, the Orange class now would throw the following error.

The type 'int' cannot be used as type parameter 'TDerieved' in the generic type or method 'IFruit<TDerieved>'. There is no boxing conversion from 'int' to 'IFruit<int>'.

Operator Overloading in interface

Let us now head back to our discussion on operator overloading. Let us consider the following interface without any generic constraints.

public interface ISequenceGenerator < T > {
    static abstract T Zero {
        get;
    }
    static abstract T One {
        get;
    }
    static abstract T operator++(T val);
}

The above code would throw the following error.

 The parameter type for ++ or -- operator must be the containing type, or its type parameter constrained to it 

As the error describes, there is no way to constrain the type T to ensure operation is supported by the type. This problem can be solved by using the Curiously Recurring Template Pattern.

public interface ISequenceGenerator < T > where T: ISequenceGenerator < T > {
    static abstract T Zero {
        get;
    }
    static abstract T One {
        get;
    }
    static abstract T operator++(T val);
}

To see the code in action, let us create an example implementation.

public struct ValGen: ISequenceGenerator < ValGen > {
    public int Value {
        get;
        set;
    }
    public static ValGen Zero => new ValGen {
        Value = 0
    };
    public static ValGen One => new ValGen {
        Value = 1
    };
    public static ValGen operator++(ValGen val) {
        return val with {
            Value = val.Value + 1
        };
    }
    public override string ToString() {
        return Value.ToString();
    }
}

Let us look into another implementation of ISequenceGenerator<TDerieved>, this time for generating Fibonacci numbers.

public struct FibonacciGenerator: ISequenceGenerator < FibonacciGenerator > {
    public int Previous {
        get;
        init;
    }
    public int Current {
        get;
        init;
    }
    public override string ToString() => Current.ToString();
    public static FibonacciGenerator Zero => new FibonacciGenerator {
        Current = 0, Previous = 0
    };
    public static FibonacciGenerator One => new FibonacciGenerator {
        Current = 1, Previous = 0
    };
    public static FibonacciGenerator operator++(FibonacciGenerator val) {
        if (val.Equals(FibonacciGenerator.Zero)) return FibonacciGenerator.One;
        else return val with {
            Previous = val.Current,
                Current = val.Previous + val.Current
        };
    }
}

We could now use the FibonacciGenerator using ++ operator generates a sequence

var fibGenerator = FibonacciGenerator.Zero;
for (var i = 0; i < 10; i++) Console.Write($ "{fibGenerator++}, ");
// Output
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34,

Doesn't the code look more readable? I think yes, though I agree that it depends on perspective. But the way I see it, static abstract members open up a whole new world opportunities for developers.


Similar Articles