Search code examples
c#oopgenerics

C#: Possible to create type hierarchy between generic wrapper types?


I've created a generic wrapper type (AnimalVehicleWrapper in the example below). I've created two separate derived types, one which holds "parent" types (animal and vehicle) and one that holds more derived types (dog and car). I'm wondering if it's possible to create a type hierarchy such that I could create a method that would define an incoming ParentWrapper parameter, which would all me to pass either that type OR the more derived DerivedWrapper.

Thank you.

using Example;

namespace Example
{
    public class Animal
    {
        public string Name { get; set; }
    }

    public class Dog : Animal
    {
        int PawSize { get; set; }
    }


    public class Vehicle
    {
        public string Name { get; set; }
    }

    public class Car : Vehicle
    {
        public int TireCount { get; set; }
    }

    public class AnimalVehicleWrapper<T1, T2>
        where T1 : Animal
        where T2 : Vehicle
    {
        public virtual T1 AnimalOrDerived { get; set; }
        public virtual T2 VehicleOrDerived { get; set; }

    }
}

public class ParentWrapper : AnimalVehicleWrapper<Animal, Vehicle>
{

}

public class DerivedWrapper : AnimalVehicleWrapper<Dog, Car>
{

}

...

    public static void DoProcessing(ParentWrapper wrapper)
    {
        // Would like to be able to pass in a ParentWrapper OR a DerivedWrapper here
    }

Solution

  • DerivedWrapper doesn't extend ParentWrapper, so this won't work as-is. It wouldn't make sense for it to: what would happen if you passed in a DerivedWrapper and the method tried to set AnimalOrDerived to a Bird?

    DoProcessing(new DerivedWrapper()); // this has an error
    
    static void DoProcessing(ParentWrapper wrapper)
    {
        wrapper.AnimalOrDerived = new Bird(); // this is fine
    }
    
    public class Bird : Animal
    {
        int TalonSize { get; set; }
    }
    

    There's nothing wrong with the DoProcessing method above: it should be possible to set ParentWrapper.AnimalOrDerived to any kind of animal. But that wouldn't be possible on a DerivedWrapper, whose AnimalOrDerived property must be a dog.

    If you don't need DoProcessing to set any of the properties on the input object, you could define a covariant interface and make the wrapper class implement it:

    DoProcessing(new DerivedWrapper()); // this is fine
    
    static void DoProcessing(IAnimalVehicleWrapper<Animal, Vehicle> wrapper)
    {
        // wrapper.AnimalOrDerived = new Bird(); // This would have an error
    }
    
    public interface IAnimalVehicleWrapper<out T1, out T2>
    {
        T1 AnimalOrDerived { get; }
        T2 VehicleOrDerived { get; }
    }
    
    public class AnimalVehicleWrapper<T1, T2> : IAnimalVehicleWrapper<T1, T2>
        where T1 : Animal
        where T2 : Vehicle
    {
        public virtual T1 AnimalOrDerived { get; set; }
        public virtual T2 VehicleOrDerived { get; set; }
    }
    

    Now, the lack of setters on the interface would prevent DoProcessing from trying to give the class an inappropriately-specific type. But it can still get values, knowing that the properties will be some kind of Animal or Vehicle. That makes it safe for outside methods to pass in any kind of IAnimalVehicleWrapper<>.

    Note that if you don't need to be more specific about IAnimalVehicleWrapper's generic types, it could just as easily be non-generic. The generic AnimalVehicleWrapper<> class would just have to implement the interface explicitly.

    DoProcessing(new DerivedWrapper());
    
    static void DoProcessing(IAnimalVehicleWrapper wrapper)
    {
        // wrapper.AnimalOrDerived = new Bird(); // this would fail
    }
    
    public interface IAnimalVehicleWrapper
    {
        Animal AnimalOrDerived { get; }
        Vehicle VehicleOrDerived { get; }
    }
    
    public class AnimalVehicleWrapper<T1, T2> : IAnimalVehicleWrapper
        where T1 : Animal
        where T2 : Vehicle
    {
        public virtual T1 AnimalOrDerived { get; set; }
        public virtual T2 VehicleOrDerived { get; set; }
    
        Animal IAnimalVehicleWrapper.AnimalOrDerived => this.AnimalOrDerived;
    
        Vehicle IAnimalVehicleWrapper.VehicleOrDerived => this.VehicleOrDerived;
    }