C# - Type Constraints and Limitations, any workaround to keep Type Safety?

567 Views Asked by At

I have a pretty common scenario about the limitations of Generic Type Constraint that would required another Generic to be defined.

It has already been discussed (Eric Lippert himself and others) but so far I haven't seen a general guideline or let's say a rule of thumb that can be applied when bumping into the following scenario:

public abstract class Class<TProperty> : Class
     where TProperty : Property<>
// Sadly the line above cannot work, although the compiler could actually infer
// the Generic Type, since defining two class definitions like:
// Considering A & B two other well-defined classes
// Class<TA> where TA : A and 
// Class<TB> where TB : B is not allowed and well-understandable
{
    protected Class(TProperty property)
    {
        if (property != null)
        {
            this._property = property;
        }
        else
        {
            throw new ArgumentNullException(@"property");
        }
    }

    private readonly TProperty _property;
    public TProperty Property
    {
        get
        {
            return this._property;
        }
    }
}
public abstract class Property<TParentClass>
// Same remark goes here
    where TParentClass : Class<>
{
    protected Property(TParentClass parent)
    {
        if (parent != null)
        {
            this._parent = parent;
        }
        else
        {
            throw new ArgumentNullException(@"parent");
        }
    }

    private readonly TParentClass _parent;
    internal TParentClass Parent
    {
        get
        {
            return this._parent;
        }
    }
}

This is fine we still have some workarounds by using interfaces or making up new base classes just as follows:

public abstract class Class
{
}
public abstract class Class<TProperty> : Class
    where TProperty : Property
{
    protected Class(TProperty property)
    {
        if (property != null)
        {
            this._property = property;
        }
        else
        {
            throw new ArgumentNullException(@"property");
        }
    }

    private readonly TProperty _property;
    public TProperty Property
    {
        get
        {
            return this._property;
        }
    }
}

public abstract class Property
{
}
public abstract class Property<TParentClass>
    where TParentClass : Class
{
    protected Property(TParentClass parent)
    {
        if (parent != null)
        {
            this._parent = parent;
        }
        else
        {
            throw new ArgumentNullException(@"parent");
        }
    }

    private readonly TParentClass _parent;
    internal TParentClass Parent
    {
        get
        {
            return this._parent;
        }
    }
}

This is fine but what happen if I want to add a new legitimate layer of inheritance?

public abstract class InheritedClass<TInheritedProperty> : Class<TInheritedProperty>
// Damn it! I wanted to be more specific but I cannot have <> (and also <,>, <,,>, etc.)
// Cannot do that without declaring another public interface... sad
// Or another non generic base class
    where TInheritedProperty : Property
{
    // But this remark cannot work here... I would have needed a "real" type not InheritedProperty<>...
    // Yeah this is it: starting to bake the noodles
    protected InheritedClass(TInheritedProperty property)
        : base(property)
    {
    }
}

public abstract class InheritedProperty<TInheritedClass> : Property<TInheritedClass>
// Same goes here
    where TInheritedClass : Class
{
    protected InheritedProperty(TInheritedClass parent)
        : base(parent)
    {
    }
}

or even worse (with a code cannot obviously compile) and go really stupid with no real type safety:

public abstract class InheritedClass2<TInheritedProperty, TInheritedPropertyClass> : Class<TInheritedProperty>
    where TInheritedProperty : InheritedProperty2<TInheritedPropertyClass, TInheritedProperty>
{
    protected InheritedClass2(TInheritedProperty property)
        : base(property)
    {
    }
}

public abstract class InheritedProperty2<TInheritedClass, TInheritedClassProperty> : Property<TInheritedClass>
    where TInheritedClass : InheritedClass2<TInheritedClassProperty, TInheritedClass>
{
    protected InheritedProperty2(TInheritedClass parent)
        : base(parent)
    {
    }
}

At that point people would usually say no, the design should not that complicated... review your business requirement and just use massively interface with composition, inheritance is not only for saving up you to write some extra code and those classes should form some sort of families, alright they do form a kind-a family.

Well, fair enough but this does not really solve the situation which I have to confess is over-exagerated but there are cases where it makes sense to have an inheritance with constraints and where those constraints have also constraints.

Yes those ones can drive you really crazy (e.g. recursive constraints) and make you pulling you hair out... but still there are situations where this can be handy, especially in regarding type-safety.

Anyway, in regards to those constraints is what the most suitable, general guideline to follow to get back on tracks, any other solutions than just using interfaces or choosing a type subset in the constructor?

2

There are 2 best solutions below

1
On

Would this work for you?

public abstract class Class<TProperty, T>
   where TProperty : Property<T>
{

}

It's not as powerful as type constructors, because the T in Property<T> cannot vary, but it doesn't seem like you're looking for that kind of flexibility anyway.

1
On

I've encountered similar issues with trying to ultra-template a quite complicated application model in C# before. I came to the conclusion that the compiler can only do so much though.

I'd only be worried about setting up quite complicated type constraints if you specifically need to do something with the type argument. Say, for example, you need to guarantee it's an IEnumerable because your Class is going to be iterating over it. In the case of these examples, you don't really need a type constraint at all.

If you find you're desperate to maintain a type constraint, though, you could try and leverage contra- and co-variance to constrain the child types, like so:

class Foo<TProperty> : Class
    where TProperty : IProperty<object>
{
    // Same as your implementation
}

interface IProperty<out T>
{
    T Property;
}

class Property<T> : IProperty
{
    // Same as your implementation
}

EDIT: Added the IProperty interface because contra- and co-varience might only be definable on a class interface rather than it's implementation