Why does a generic type constraint result in a no implicit reference conversion error?
I have created a couple of interfaces and generic classes for working with agenda appointments:
interface IAppointment<T> where T : IAppointmentProperties
{
T Properties { get; set; }
}
interface IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
DateTime Date { get; set; }
T Appointment { get; set; }
}
interface IAppointmentProperties
{
string Description { get; set; }
}
class Appointment<T> : IAppointment<T> where T : IAppointmentProperties
{
public T Properties { get; set; }
}
class AppointmentEntry<T> : IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
public DateTime Date { get; set; }
public T Appointment { get; set; }
}
class AppointmentProperties : IAppointmentProperties
{
public string Description { get; set; }
}
I'm trying to use some constraints on the type parameters to ensure that only valid types can be specified. However, when specifying a constraint defining that T
must implement IAppointment<IAppointmentProperties>
, the compiler gives an error when using a class that is Appointment<AppointmentProperties>
:
class MyAppointment : Appointment<MyAppointmentProperties>
{
}
// This goes wrong:
class MyAppointmentEntry : AppointmentEntry<MyAppointment>
{
}
class MyAppointmentProperties : AppointmentProperties
{
public string ExtraInformation { get; set; }
}
The error is:
The type 'Example.MyAppointment' cannot be used as type parameter 'T' in the generic type or method 'Example.AppointmentEntry<T>'. There is no implicit reference conversion from 'Example.MyAppointment' to 'Example.IAppointment<Example.IAppointmentProperties>'.
Could anybody explain why this does not work?
Solution 1:
Let's simplify:
interface IAnimal { ... }
interface ICage<T> where T : IAnimal { void Enclose(T animal); }
class Tiger : IAnimal { ... }
class Fish : IAnimal { ... }
class Cage<T> : ICage<T> where T : IAnimal { ... }
ICage<IAnimal> cage = new Cage<Tiger>();
Your question is: why is the last line illegal?
Now that I have rewritten the code to simplify it, it should be clear. An ICage<IAnimal>
is a cage into which you can place any animal, but a Cage<Tiger>
can only hold tigers, so this must be illegal.
If it were not illegal then you could do this:
cage.Enclose(new Fish());
And hey, you just put a fish into a tiger cage.
The type system does not permit that conversion because doing so would violate the rule that the capabilities of the source type must not be less than the capabilities of the target type. (This is a form of the famous "Liskov substitution principle".)
More specifically, I would say that you are abusing generics. The fact that you've made type relationships that are too complicated for you to analyze yourself is evidence that you ought to simplify the whole thing; if you're not keeping all the type relationships straight and you wrote the thing then your users surely will not be able to keep it straight either.
Solution 2:
There is already a very good answer from Eric. Just wanted to take this chance to talk about the Invariance, Covariance, and Contravariance here.
For definitions please see https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance
Let's say there is a zoo.
abstract class Animal{}
abstract class Bird : Animal{}
abstract class Fish : Animal{}
class Dove : Bird{}
class Shark : Fish{}
The zoo is relocating, so its animals need to be moved from the old zoo to the new one.
Invariance
Before we move them, we need to put the animals into different containers. The containers all do the same operations: put an animal in it or get an animal out from it.
interface IContainer<T> where T : Animal
{
void Put(T t);
T Get(int id);
}
Obviously, for fish we need a tank:
class FishTank<T> : IContainer<T> where T : Fish
{
public void Put(T t){}
public T Get(int id){return default(T);}
}
So the fish can be put in and get out from the tank(hopefully still alive):
IContainer<Fish> fishTank = new FishTank<Fish>(); //Invariance, the two types have to be the same
fishTank.Put(new Shark());
var fish = fishTank.Get(8);
Suppose we are allowed to change it to IContainer<Animal>
, then you can accidentally put a dove in the tank, in which case tragedy will occur.
IContainer<Animal> fishTank = new FishTank<Fish>(); //Wrong, some animal can be killed
fishTank.Put(new Shark());
fishTank.Put(new Dove()); //Dove will be killed
Contravariance
To improve efficiency, the zoo management team decides to separate the load and unload process (management always does this). So we have two separate operations, one for load only, the other unload.
interface ILoad<in T> where T : Animal
{
void Put(T t);
}
Then we have a birdcage:
class BirdCage<T> : ILoad<T> where T : Bird
{
public void Put(T t)
{
}
}
ILoad<Bird> normalCage = new BirdCage<Bird>();
normalCage.Put(new Dove()); //accepts any type of birds
ILoad<Dove> doveCage = new BirdCage<Bird>();//Contravariance, Bird is less specific then Dove
doveCage.Put(new Dove()); //only accepts doves
Covariance
In the new zoo, we have a team for unloading animals.
interface IUnload<out T> where T : Animal
{
IEnumerable<T> GetAll();
}
class UnloadTeam<T> : IUnload<T> where T : Animal
{
public IEnumerable<T> GetAll()
{
return Enumerable.Empty<T>();
}
}
IUnload<Animal> unloadTeam = new UnloadTeam<Bird>();//Covariance, since Bird is more specific then Animal
var animals = unloadTeam.GetAll();
From the team's point of view, it does not matter what it is inside, they just unload the animals from the containers.
Solution 3:
Because you declared your MyAppointment
class using the concrete type rather than the interface. You should declare as follows:
class MyAppointment : Appointment<IAppointmentProperties> {
}
Now the conversion can occur implicitly.
By declaring AppointmentEntry<T>
with the constraint where T: IAppointment<IAppointmentProperties>
you are creating a contract whereby the unspecified type for AppointmentEntry<T>
must accommodate any type that is declared with IAppointmentProperties
. By declaring the type with the concrete class you have violated that contract (it implements a type of IAppointmentProperties
but not any type).