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?
Interface Type Constraint You can constrain the generic type by interface, thereby allowing only classes that implement that interface or classes that inherit from classes that implement the interface as the type parameter.
The where clause in a generic definition specifies constraints on the types that are used as arguments for type parameters in a generic type, method, delegate, or local function. Constraints can specify interfaces, base classes, or require a generic type to be a reference, value, or unmanaged type.
Declaring those constraints means you can use the operations and method calls of the constraining type. If your generic class or method uses any operation on the generic members beyond simple assignment or calling any methods not supported by System. Object, you'll apply constraints to the type parameter.
C# allows you to use constraints to restrict client code to specify certain types while instantiating generic types. It will give a compile-time error if you try to instantiate a generic type using a type that is not allowed by the specified constraints.
The constraining interface can also be generic. In a nullable context in C# 8.0, T may be a nullable reference type, a non-nullable reference type, or a value type. T may not be a nullable value type.
The use of a generic type parameter as a constraint is useful when a member function with its own type parameter has to constrain that parameter to the type parameter of the containing type, as shown in the following example: public class List<T> { public void Add<U> (List<U> items) where U : T {/*...*/}
If client code uses a type that doesn't satisfy a constraint, the compiler issues an error. Constraints are specified by using the where contextual keyword.
Constraints can specify interfaces, base classes, or require a generic type to be a reference, value or unmanaged type. They declare capabilities that the type argument must possess. For example, you can declare a generic class, MyGenericClass, such that the type parameter T implements the IComparable<T> interface:
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.
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://learn.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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With