AJ Henderson
AJ Henderson

Reputation: 1190

Can not use second level derived class as parameter of generic base class type

I have an interesting situation where certain things are working but others are not and I'm not sure why. Below is code that approximates my situation. I've got a static in a repository that takes a generic type which is implemented by a base type of object. I then have two levels of derived classes based on that generic type. The first level of derived class fills out the generic parameters of the base type and works fine, however any class that derives from the class that filled out the generic parameters does not work in place of the base class it is derived from.

public class Vehicle<TVehicleType, TStorage>
{
}

public class Car : Vehicle<Car, ParkingLot>
{
}

public class PickupTruck : Car
{
}

public class Dealership <TDrivableVehicle>
{
    public static TDrivableVehicle GetVehicle<TVehicleType, TStorage>(TStorage lot)
         where TDrivableVehicle : Vehicle<TVehicleType, TStorage>, new()
    {
    }
}

public class CarDealership : Dealership<Car>
{
    public static Car GetDrivableVehicle(aCarParkingLot)
    {
        return Dealership.GetDrivableVehicle<Car, CarParkingLot>(aCarParkingLot); <-- Works fine
    }
}

public class PickupTruckDealership : CarDealership
{
    public static PickupTruck GetDrivableVehicle(aCarParkingLot)
    {
        return Dealership.GetDrivableVehicle<PickupTruck, CarParkingLot>(aCarParkingLot); <-- fails
    }
}

Certain aspects seem to work correctly in terms of PickupTruck understanding its generic base, but extensions (not shown here) and passing the type to a type parameter specifically do not(the GetDrivableVehicle call). My guess is that the extension method is related to the type parameter issue since it would need to figure out the type.

Any ideas why this doesn't work and/or what can be done to work around it?

Upvotes: 4

Views: 3302

Answers (3)

Andras Zoltan
Andras Zoltan

Reputation: 42333

Having rewritten your code to a point where I can get it to fail where you say it will - the problem is exactly as Thom Smith says: PickupTruck inherits Car and therefore is a Vehicle<Car, ParkingLot>, not a Vehicle<PickupTruck, ParkingLot>. Also, because of it's generic inheritance it is impossible for it to be anything else other than that.

I know your code is only a boiled down representation of the problem you're having - but if it's close enough, then there might be some useful observations we can make here about the overall architecture.

I'm not against generic bases being aware of their derived counterparts - indeed it's particularly useful for factories; however it almost always instantly rules out any further inheritance.

You're trying to encode far too much information at the type level; and apart from the number of angle brackets we see here it's actually hinted at by the slightly incongruous nature that Vehicle<TVehicleType, TStorage> is determining the type of storage that it can be stored within.

To me, that just doesn't make any sense because let's say we have a ParkingLot today, but tomorrow we also get a Hangar (for cars being stored under cover) - this will require a whole new swathe of vehicle types which are unequal by virtue of the fact that we also have the derived type being passed to the base Vehicle<TDerived, ...> - ergo ParkingLotCar and HangarCar can never be equivalent, even if two instances both represent the same make/model etc.

So, anticipating that, you have gone for inheritance where you have a common Car, but of course at that point any inheritance is pointless because Car is a Vehicle<Car,...> so anything derived from it must also be. Only with Multiple inheritance could that not be the case, but even with that it doesn't get around the whole ParkingLot question.

Ask yourself why does Vehicle<,> need to know about the deriving type? Is it so you can have a single factory method? In that case, you should put it into the Dealership, or the ParkingLot types; not the Vehicle base:

public interface IVehicle {}
public interface ICar : IVehicle {} //because pickup trucks share some car traits
public interface IPickup : ICar, IVehicle {}
public interface IStorage {}

public class Car : ICar, IVehicle {}
public class Pickup : IPickup, ICar, IVehicle {}

public class ParkingLot : IStorage {}
public class Hangar : IStorage {}

public class Dealership
{
  public static TVehicle GetVehicle<TVehicle>(IStorage storage)
    where TVehicle : IVehicle, new()
  {

  }
}

//now you can specialise if you really need to

public class CarDealership
{
    public static Car GetVehicle(IStorage storage)
    {
        return Dealership.GetVehicle<Car>(storage);
    }
}

public class PickupDealership
{
    public static Pickup GetVehicle(IStorage storage)
    {
        return Dealership.GetVehicle<Pickup>(storage);
    }
}

Now you have the runtime relationships between your vehicle types; allowing different concrete types to share traits such as those on the ICar or IPickup interfaces; but you've broken the relationship between the vehicle and it's storage so you can get an IVehicle from a FootballPitch or drive it off the pier to the RiverBed if you need to.

Upvotes: 3

Ryan Bennett
Ryan Bennett

Reputation: 3432

You are trying to use a derived class in place of your base class. This will not work. You should be able to use your BASE class instead of a more derived class, not the other way around. You could convert Car to an interface (ICar) and have PickupTruck implement ICar. That would work.

Read up on contravariance vs covariance. http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx

Upvotes: 0

Thom Smith
Thom Smith

Reputation: 14086

The way you have it set up, a PickupTruck is not a Vehicle<PickupTruck, CarParkingLot>, but merely a Vehicle<Car, CarParkingLot>. This sort of infinite template recursion can be very confusing to work with. You can solve this particular issue by declaring PickupTruck : Vehicle<PickupTruck, CarParkingLot>, by mucking around with co[ntra]variance, or by refactoring your class hierarchy to avoid the confusing pattern.

Upvotes: 1

Related Questions