While reading forums and other discussions, I often notice that it is difficult for developers to see practical applications of Covariance and Contravariance in Generics. This is due in part because of our Greco-Roman approach to learning. In this approach you learn as much as you can and when you face a real life problem you will try to dig through your memory archives in a futile attempt to find needed pieces of information or theoretical concepts that you have learned 10-20 years ago. This approach has not served me well over the years. I had 5 years of advance chapters of math in college but most of what I remember to this day is the names of those chapters and not much of the content. I recently needed to use Fourier series and could not remember the formula, thanks to God for giving wonderful ideas to some people who invented search engines. What we would be doing these days without search engines - I don’t know.
Therefore I am a firm believer of a different approach often seen in some middle eastern and Asian cultures. Face the problem first, try to solve it and learn as you go. I found that knowledge retention in this approach is so much greater. In addition you see practical application of a concept right there. Seeing this becomes even easier when your salary depends on solving such problems :). This was the case with Covariance and Contravariance for me. I vaguely remembered the concept from computer science lectures, while the concept is simple, it is easy to forget it without needed practice. So when I faced a real problem, initially I have not associated it with the concept I learned earlier, but I went on to discovery route and found the answer!
The “Aha” moment filled my brain :) “this is something I learned before” I thought to myself. So I had to re-introduce myself to the concept.
To cover some of these learning gaps I decided to write this short article starting it with a discussion about a problem!
Let’s say there is some hierarchy of classes in a program and there is class Child which inherits from class Parent. Now there is a need to implement a method which will take a collection of objects of type Parent and do something very generic over these objects. So the method signature might look something like this:
public void DoSomething(List<Parent> collection) { foreach (Parent p in collection) { p.SomeMethod(); } }
public void DoSomething(Parent object) { object.SomeMethod(); }Wait a minute, isn’t it a simple polymorphism? Yes it is. But when will passing List<Child> not work? In all versions of C# prior to version C# 4.0 . Why? Because it doesn’t support contravariance for generic types.While Parent and Child polymorphic through inheritance, List<Parent> and List<Child> are not “polymorphic” :). Let me get back to this a little later, but for now let me give a real world example first.
List<Child> list = new List<Child>(); // populate list ... // call DoSomething which accepts List<Parent> DoSomething(list.Select(t => t as Parent));
“Within the type system of a programming language, covariance and contravariance refers to the ordering of types from narrower to wider and their interchangeability or equivalence in certain situations (such as parameters, generics, and return types).
- covariant: converting from wider (double) to narrower (float).
- contravariant: converting from narrower (float) to wider (double).
- invariant: Not able to convert.”