Friday, January 17, 2014

Covariance and Contravariance



Covariance means that you can use IEnumerable<string> in place where IEnumerable<object> is expected. Contravariance allows you to pass IComparable<object> as an argument of a method taking IComparable<string>

Assuming that every Animal has a Name, we can write the following function that prints names of all animals in some collection:

void PrintAnimals(IEnumerable<Animal> animals) {
  for(var animal in animals)
    Console.WriteLine(animal.Name);
}

It is important to note that IEnumerable<Animal> can be only used to read Animal values from the collection (using the Current property of IEnumerator<Animal>). There is no way we could write Animal back to the collection - it is read-only. Now, if we create a collection of cats, can we use PrintAnimals to print their names?

IEnumerable<Cat> cats = new List<Cat> { new Cat("Troublemaker") };
PrintAnimals(cats);

If you compile and run this sample, you'll see that the C# compiler accepts it and the program runs fine. When calling PrintAnimals, the compiler uses covariance to convert IEnumerable<Cat> to IEnumerable<Animal>. This is correct, because the IEnumerable interface is marked as covariant using the out annotation. When you run the program, the PrintAnimals method cannot cause anything wrong, because it can only read animals from the collection. Using a collection of cats as an argument is fine, because all cats are animals.

Contravariance

Contravariance works the other way than covariance. Let's say we have a method that creates some cats and compares them using a provided IComparer<Cat> object. In a more realistic example, the method might, for example, sort the cats:

void CompareCats(IComparer<Cat> comparer) {
  var cat1 = new Cat("Otto");
  var cat2 = new Cat("Troublemaker");
  if (comparer.Compare(cat2, cat1) > 0)
    Console.WriteLine("Troublemaker wins!");
}

The comparer object takes cats as arguments, but it never returns a cat as the result. You could say that it is a write-only in the way in which it uses the generic type parameter. Now, thanks to contravariance, we can create a comparer that can compare animals and use it as an argument to CompareCats:

IComparator<Animal> compareAnimals = new AnimalSizeComparator();
CompareCats(compareAnimals);

The compiler accepts this code because the IComparer interface is contravariantand its generic type parameter is marked with the in annotation. When you run the program, it also makes sense. The compareAnimals object that we created knows how to compare animals and so it can certainly also compare two cats. A problem would be if we could read a Cat from IComparer<Cat> (because we couldn't get Cat from IComparer<Animal>!), but that is not possible, because IComparer is write-only.

 

No comments:

Post a Comment