Question

I know how to force a type parameter to be a subtype of another type:

public interface IMapping<T2> 
{
    public void Serialize<T3>(T3 obj) 
        where T3 : T2;
}
...

var mapping = MapManager.Find<Truck>();
mapping.Serialize(new TonkaTruck());

Is there a way to force a type parameter to be a supertype of another type?

public interface IMapping<T2>
{
    public void IncludeMappingOf<T1>() 
        where T2 : T1;   // <== doesn't work
}
...

var mapping = MapManager.Find<Truck>();

// Truck inherits Vehicle    
// Would like compiler safety here:
mapping.IncludeMappingOf<Vehicle>(); 

mapping.Serialize(new TonkaTruck());

Currently, I'm having to compare T1 and T2 at runtime using IsSubclassOf inside IncludeMappingOf. A compile-safe solution would be preferable. Any ideas?

EDIT: Changed the example to be less design-smelly.

NOTE: The linked question is quite similar, but no suitable answer is given. Hopefully this question will shed some light on that one as well.

EDIT #2:

Simpler example:

public class Holder<T2>
{
    public T2 Data { get; set; }

    public void AddDataTo<T1>(ICollection<T1> coll)
        //where T2 : T1    // <== doesn't work
    {
        coll.Add(Data);   // error
    }
}

...
var holder = new Holder<Truck> { Data = new TonkaTruck() };
var list = new List<Vehicle>();
holder.AddDataTo(list);

Compiler: Argument type 'T2' is not assignable to parameter type 'T1'. Yes I know that, I'm trying to get the compiler to allow only cases where T2 IS assignable to parameter type T1!

Was it helpful?

Solution 3

You can use extension methods to come close to what you want. Using your holder example it would be:

public class Holder<T2>
{
    public T2 Data { get; set; }
}

public static class HolderExtensions
{
    public static void AddDataTo<T2, T1>(this Holder<T2> holder, ICollection<T1> coll)
        where T2 : T1
    {
        coll.Add(holder.Data);
    }
}

That then allows your example calling code to compile without error:

var holder = new Holder<Truck> { Data = new TonkaTruck() };
var list = new List<Vehicle>();
holder.AddDataTo(list);

The mapping example is complicated by the fact that it is an interface. It may be necessary to add an implementation method to the interface if there is no way to implement the extension method from the existing interface. That means you will still need a runtime check, but callers can get the good syntax and compile time checking. That would be something like:

public interface IMapping<T2>
{
    void IncludeMappingOf(Type type);
}

public static class MappingExtensions
{
    public static void IncludeMappingOf<T2, T1>(this IMapping<T2> mapping)
        where T2 : T1
    {
        mapping.IncludeMappingOf(typeof(T1));
    }
}

Unfortunatly, the IncludeMappingOf does not have a parameter of type T1 so the type parameters cannot be inferred. You are forced to specify both types when calling it:

var mapping = MapManager.Find<Truck>();
mapping.IncludeMappingOf<Truck, Vehicle>();
mapping.Serialize(new TonkaTruck());

That can often be worked around by changing the API to include a parameter (i.e. truckMapping.IncludeMappingOf(vehicleMapping)), changing which method/class the parameter is on or in fluent APIs creating chains (i.e. mapping.Of<Vehicle>().Include()).

OTHER TIPS

While w0lf's answer gives a direct solution, I want to give some background explanation.

When you write something like

class C<A> where A : B

or

void F<A>() where A : B

the constraints of the form A : B must have A as one of the generic type parameters on the class, interface, method, etc. being declared.

The error you are facing is not because you've placed a generic type parameter of the current declaration on the right side of the colon (that's legal) - it's because you've placed a generic type parameter of an outer declaration (not of the current declaration) on the left side of the colon.

If you want to form a constraint A : B on some declaration, A must be introduced on that declaration and the scope of A must be less than or equal to the scope of B. The reason this is a pragmatic language restriction is that, for any generic type parameter T, it isolates any reasoning about constraints on the type T to the single declaration where T is being introduced.

Declare both generic types and the generic constraint at class(interface) level:

public interface IMapping<T1, T2> where T2 : T1
{
    void IncludeMapping(IMapping<T1, T2> otherMapping);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top