Home > other >  Restrict a generic type parameter to the callers static type in C#?
Restrict a generic type parameter to the callers static type in C#?

Time:12-30

An immutable sub class inherits a property from an abstract base class (for some kind of a strategy pattern). I am looking for a reusable implementation of an immutable setter method WithValue in the base class that returns the static type of the caller, without having to surface any generics in the fluent syntax.

public abstract class BaseX
{
    public string Value { get; init; }

    public BaseX(string value)
    {
        Value = value;
    }

    public T WithValue<T>(string value) where T : BaseX, new()
    {
        T newX = new()
        {
            Value = value
        };
        return newX;
    }

}

public class SubA : BaseX
{

    public SubA() : base("A") { }

    public void DoAishStuff() { ... }
}

What I want to accomplish is this:

SubA a = new();
a.WithValue("foo").DoAishStuff(); // fluent syntax

But this actually requires a type argument a.WithValue<SubA>(...), which is not nice to write, and would allow to use other subtypes like SubB.

I can accomplish the desired syntax with an extension method:

public static class BaseXExtensions
{
    public static T WithValue<T>(this T self, string value) where T : BaseX, new()
    {
        T obj = new()
        {
            Value = value
        };
        return obj;
    }
}

However, it seems a little strange that I can't integrate this mechanism into the base class itself. Basically what I'm looking for is a constraint like where T : this. Are there other ways to achieve this?

CodePudding user response:

Make the base class itself a generic, and define its constraint in terms of itself.

public abstract class BaseX<T> where T : BaseX<T>, new()
{
    public string Value { get; init; }

    protected BaseX(string value) => Value = value;

    public T WithValue(string value) => new() { Value = value };
}

Your subclasses then inherit from that base class:

public class SubA : BaseX<SubA>
{
    public SubA() : base("A") { }

    public void DoAishStuff() { ... }
}

I sometimes use both a non-generic base class and a generic base class that inherits from the non-generic. This allows you to handle edge cases with BaseX and normal cases with BaseX<T>.

Consider,

public abstract class BaseX
{
    public string Value { get; protected init; }

    protected BaseX(string value) => Value = value;
}

public abstract class BaseX<T> : BaseX where T : BaseX<T>, new()
{
    protected BaseX(string value) : base(value)
    {}

    public T WithValue(string value) => new() { Value = value };
}

public class SubB : BaseX
{
    public SubB() : base("B") { }

    // one off way of doing WithValue() for BaseX
    public SubB WithValue(string value) => new SubB("'"   value   "'");
}

public class SubA : BaseX<SubA>
{
    public SubA() : base("A") { }

    public void DoAishStuff() { ... }
}

CodePudding user response:

You can try leveraging covariant return types (available since C# 9):

public abstract class BaseX
{
    public string Value { get; init; }

    public BaseX(string value)
    {
        Value = value;
    }

    public abstract BaseX WithValue(string value);
    
    protected T WithValueInternal<T>(string value) where T : BaseX, new()
    {
        T newX = new()
        {
            Value = value
        };
        return newX;
    }
}

public class SubA : BaseX
{
    public SubA() : base("A") { }

    public void DoAishStuff() { }
    
    public override SubA WithValue(string value) => WithValueInternal<SubA>(value);
}

Which allows a.WithValue("foo").DoAishStuff();

You can introduce an interim generic class (with curiously recurring template pattern) so you don't need to override the WithValue method in inheritors:

public abstract class BaseXX<T>: BaseX where T : BaseX, new()
{
    public BaseXX(string value) : base("A") { }
    public override T WithValue(string value) => WithValueInternal<T>(value);
}

public class SubA : BaseXX<SubA>
{
    public SubA() : base("A") { }

    public void DoAishStuff() { }
}

Also note that records support very similar nondestructive mutation via with keyword.

  • Related