Covariance and Contravariance - Just different mechanisms for invoking guaranteed base class behavior?

StackOverflow https://stackoverflow.com/questions/19262845

  •  30-06-2022
  •  | 
  •  

Question

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.

Was it helpful?

Solution 2

I'm having a struggle understanding these two concepts.

Yes you are. Many people do.

But I think after many videos and SO QA's, I have it distilled down to its simplest form:

You have not.

Covariance means that a sub-type can do what its base-type does.

No. That's the Liskov Substitution Principle.

Contravariance means you can treat a sub-type the same way you would treat its base-type.

No. That's just re-stating what you said for covariance.

The real distillation of covariance and contravariance is:

  • A covariant conversion preserves the direction of another conversion.

  • A contravariant conversion reverses the direction of another conversion.

Dog is convertible to Animal. IEnumerable<Dog> is convertible to IEnumerable<Animal>. The direction is preserved, so IEnumerable<T> is covariant. IComparable<Animal> is convertible to IComparable<Dog>, which reverses the direction of the conversion, so it is contravariant.

I understand mathematically what covariance means, and so I guess it's the same in compsci.

Just to be clear: mathematicians use "variance" to mean a bunch of different things. The meaning that is common to mathematics and computer science is the category theory definition.

In C# it's just a matter of where and in what ways these two types of relationships are supported?

Mathematically, variance tells you about whether a relation is preserved or reversed by a mapping. If we have the mapping T --> IEnumerable<T> and the relation "is convertible to via identity or reference conversion" then it is the case that in C#, if X relates to Y then IE<X> relates to IE<Y>. The mapping is therefore said to be covariant with respect to the relation.

what is it that these features are trying to accomplish by supporting them?

People frequently requested "I have a method that takes a sequence of animals and I have a sequence of turtles in hand; why do I have to copy the sequence to a new sequence to use the method?" That's a reasonable request, we got it frequently, and we got it a lot more frequently after LINQ made it easier to work with sequences. It's a generally useful feature that we could implement at a reasonable cost, so we implemented it.

OTHER TIPS

Variance - refers to how complex types (arrays, lists, delegates, generics) relate to the direction of subtyping of their underlying types.

In other words it is about in what direction is allowed to implicitly cast complex types.

Example of the relation of two complex types (delegates) according to their underlying types Animal and Cat.

Covariance is a preserved direction of implicit casting as to subtyping direction (Animal<-Cat)

// Covariance based on type of return param of delegate
var catDelegate = new Func<Cat>(delegate {return null;});

// Allowed implicit casting from delegate based on Cat return param 
// to delegate based on Animal return param 
Func<Animal> animalDelegate = catDelegate;

Contravariance is a reversed direction of implicit casting as to subtyping direction (Animal->Cat)

// contravariance based on type of passed arguments of delegate
var animalDelegate = new Action<Animal>(delegate{});

// Allowed implicit casting from delegate based on Animal passed param 
// to delegate based on Cat passed param
Action<Cat> catDelegate = animalDelegate;

Invariance is a unsupported implicit casting (in any direction)

Generic lists are invariant

List<Animal> animals = new List<Cat>(); // error!
List<Cat> animals = new List<Animal>(); // error!

Examples of supported variance in C#

Arrays are covariant

Animal[] animals = new Cat[10]; // possible

Generic IEnumerable is covariant

IEnumerable<Animal> animals = new List<Cat>(); // possible

I think we are limiting the scope of covariance and contravariance if we are just thinking in terms of base type and sub type and how base type behavior is called. Real merit of contravarinace and covariance comes in terms of what kind of types (projections as explained by Eric lippert http://blogs.msdn.com/b/ericlippert/archive/2009/11/30/what-s-the-difference-between-covariance-and-assignment-compatibility.aspx) can be created using them. Following faqs on variance should be able to clear your doubt. http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top