I'm having a struggle understanding these two concepts. But I think after many videos and SO QA's, I have it distilled down to its simplest form:
Covariant - Assumes a sub-type can do what its base-type does.
Contravariant - Assumes you can treat a sub-type the same way you would treat its base-type.
Supposing these three classes:
class Animal
{
void Live(Animal animal)
{
//born!
}
void Die(Animal animal)
{
//dead!
}
}
class Cat : Animal
{
}
class Dog : Animal
{
}
Covariant
Any animal can do what animals do.
Assumes a sub-type can do what its base-type does.
Animal anAnimal = new Cat();
anAnimal.Live();
anAnimal.Die();
Animal anotherAnimal = new Dog();
anotherAnimal.Live();
anotherAnimal.Die();
Contravariant
Anything you can do to an animal, you can do to any animal.
Assumes you can treat a sub-type the same way you would treat its base-type.
Action<Animal> kill = KillTheAnimal;
Cat aCat = new Cat();
KillTheCat(kill, aCat);
Dog = new Dog();
KillTheDog(kill, aDog);
KillTheCat(Action<Cat> action, Cat aCat)
{
action(aCat);
}
KillTheDog(Action<Dog> action, Dog aDog)
{
action(aDog);
}
void KillTheAnimal(Animal anAnimal)
{
anAnimal.Die();
}
Is this correct? It seems like at the end of the day, what covariance and contravariance allow you to do is simply use behavior you would naturally expect, i.e. every type of animal has all animal characteristics, or more generally - all sub-types implement all features of their base-type. Seems like it's just allowing for the obvious - they just support different mechanisms that allow you to get at that inherited behavior in different ways - one converts from sub-type to base-type (Covariance) and the other converts from base-type to sub-type (Contravariance), but at its very core, both are just allowing behavior of the base class to be invoked.
For example in the cases above, you were just allowing for the fact that the Cat
and the Dog
sub-types of Animal
both have the methods Live
and Die
- which they very naturally inherited from their base class Animal
.
In both cases - covariance and contravariance - we are allowing for invocation of general behavior that is guaranteed because we have made sure that the target the behavior is being invoked on inherits from a specific base class.
In the case of Covariance, we are implicitly casting a sub-type to its base-type and calling the base-type behavior (doesn't matter if the base-type behavior is overridden by the sub-type...the point is, we know it exists).
In the case of Contravariance, we are taking a sub-type and passing it to a function we know only invokes base-type behavior (because the base-type is the formal parameter type), so we are safe to cast the base-type to a sub-type.