Why does IParseable<T> need to be recursive?

595 Views Asked by At

Why does IParseable<T> place the T : IParseable<T> constraint on T? What is this recursive constraint needed for? enter image description here

1

There are 1 best solutions below

1
On

This is so called Curiously_recurring_template_pattern (CRTP) and as far as I understand it is not strictly required (and actually it can't enforce the "correct" behaviour) but as mentioned in the Preview Features in .NET 6 – Generic Math article it's usage is a hint to support one of the quite important scenarios for static abstract interface members calls - using it via generic interfaces:

This general pattern is sometimes referred to as the Curiously Recurring Template Pattern (CRTP) and is key to allowing the feature to work.

Let's imagine the following interface:

public interface IParseable1<TSelf>
    // where TSelf : IParseable1<TSelf>
{
    static abstract TSelf Parse(string s, IFormatProvider? provider);

    static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TSelf result);
}

and next method:

static T InvariantParse<T>(string s)
    where T : IParseable1<T>
{
    return T.Parse(s, CultureInfo.InvariantCulture);
}

If we implement next classes pair:

class MyClass1
{
}

class MyClass : IParseable1<MyClass1>
{
    public static MyClass1 Parse(string s, IFormatProvider? provider)
    {
        throw new NotImplementedException();
    }

    public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out MyClass1 result)
    {
        throw new NotImplementedException();
    }
}

Then next call will not compile:

var x = InvariantParse<MyClass>("");

And while CRTP does not prevent from "incorrect" usage (i.e. class MyClass1 : IParsable<MyClass1> will allow class MyClass : IParsable<MyClass1>, as far as I remember there is no language construct to enforce such behaviour) it is heavily hinting on the desired usage.

Note that class MyClass : IParsable<MyClass1> can be used in similar method but it becomes quite clumsy (partly due to specifics of C# generic types inference):

public static TOut InvariantParseTwoGenerics<T, TOut>(string s)
    where T : IParseable1<TTO>
{
    return T.Parse(s, CultureInfo.InvariantCulture);
}

var x1 = InvariantParseTwoGenerics<MyClass, MyClass1>("");

Post from Stephen Cleary discussing CRTP.

UPD

There was a breaking change between .NET 6 and .NET 7 - System.IParseable was renamed to System.IParsable