1

I'm wondering if there is a way to constrain the implementations of a generic type by asking Not to implement a specific interface

Something like

public class PrivateGarage<TVehicle> where TVehicle : **not** Itruck
{
...
}

This might works but it's less elegant

public class PrivateGarage<TVehicle>
{
   public PrivateGarage()
   {
      if(typeof(TVehicle) is Itruck)
      {
         throw new ArgumentException("Truck is not a valid type for private garage");
      }
   }
}
Michele Bortot
  • 195
  • 2
  • 9
  • 3
    No, there's no "negative" constraint. An `ArgumentException` is fine, though you should also reconsider your design, since explicitly wanting to exclude an *interface* is smelly: it suggests the interface is being used not as a contract for available operations but as an implementation distinguisher, which is probably better done by excluding a specific base class or checking a `SupportsPrivateGarageSomethingSomething` property at runtime. – Jeroen Mostert Sep 16 '22 at 15:23
  • 2
    No, the correct way to do this would be to have an interface specifically for this, like `ICanGoInGarage`, which may itself also implement `IVehicle` – DavidG Sep 16 '22 at 15:31
  • Your requirement might also break LSP (especially when throwing the exception). See: [What is an example of the Liskov Substitution Principle?](https://stackoverflow.com/q/56860/880990). – Olivier Jacot-Descombes Sep 16 '22 at 15:46
  • What benefit is there in generics of requiring a type _not_ implement an interface? – D Stanley Sep 16 '22 at 15:58

3 Answers3

4

There is an approach you can take to solve this, but it only works well if there is only one discriminant to worry about.

Let's suppose the base interface is IVehicle:

public interface IVehicle
{
    public void Park();
}

In your case, the discriminant is whether or not the vehicle can go in the garage, i.e., is it a "private vehicle"?

The following two interfaces can represent the discriminant:

public interface ICommercialVehicle: IVehicle
{
}
                                                                     
public interface IPrivateVehicle: IVehicle
{
}

Now you can represent a private garage by requiring IPrivateVehicle rather than IVehicle:

public class PrivateGarage<T> where T: IPrivateVehicle
{
    readonly List<IPrivateVehicle> _vehicles = new();

    public void Park(T vehicle)
    {
        _vehicles.Add(vehicle);
    }
}

Suppose you also have a Truck type that does not inherit directly or indirectly from IPrivateVehicle. In that case if you try to create a PrivateGarage<Truck> you'll get a compile error.

This approach does not scale at all well, so it's probably better to take the approach of using properties and checking them at runtime.


A slightly different approach is to use interfaces as "Tags". I must stress that this is considered by many to be a bad design.

To do that you'd use an IPrivateVehicle tag as a kind of attribute.

Then the hierarchy would be like this:

public interface IVehicle
{
    public void Park();
}

public interface IPrivateVehicle {} // Tag interface. Considered bad design.

public class PrivateGarage<T> where T: IPrivateVehicle, IVehicle
{
    readonly List<IVehicle> _vehicles = new();

    public void Park(T vehicle)
    {
        _vehicles.Add(vehicle);
    }
}

public interface ICar: IVehicle, IPrivateVehicle
{
}

public interface ITruck : IVehicle
{
}

Then if you had concrete classes implementing ICar and ITruck called Car and Truck respectively:

var carGarage   = new PrivateGarage<Car>();   // Compiles
var truckGarage = new PrivateGarage<Truck>(); // Does not compile.
  • An advantage of this approach is that you can use many tags.
  • An disadvantage of this approach is that you can use many tags. ;)
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
3

No, there isn't. The only way to specify constraints is inclusive. There is no way to exclude specific subtypes. See the documentation for the list of permitted types of constraints.

The reason, most likely, is such a constraint would break polymorphism. If it were possible, it would mean that instances of a specific descendant of the actual type parameter, and its all descendants would, could not be passed to the generic class.

A possible alternate way to impose such a constraint is to introduce properties at an IVehicle interface such as:

public interface IVehicle
{
    bool CanCarryPassengers { get; }
    bool CanCarryCargo { get; }
}

However, there's much more to check for a hypothetical PrivateGarage, so in the reality, the conditional to allow a particular vehicle in the garage would be much more complicated than a simple negative constraint.

Ondrej Tucny
  • 27,626
  • 6
  • 70
  • 90
2

No, there is no weay to exclude a type, constraints don't work that way.

A common solution would be to have an interface specifically for this, like IVehicleThatCanGoInGarage, which may itself also implement IVehicle

public interface IVehicleThatCanGoInGarage : IVehicle
{}

public class PrivateGarage<TVehicle> where TVehicle : IVehicleThatCanGoInGarage
{
    ...
}
DavidG
  • 113,891
  • 12
  • 217
  • 223