The concept of covariance and contravariance is fairly straightforward for basic datatypes, but it gets a lot less intuitive when you start dealing with generic collections. I recently, finally, got my head around it.
Here are some simple cases with simple classes in C# (for brevity, I’m leaving out the rules for basic datatypes). A covariant conversion is when you convert from a derived class to a class that it inherits from. It requires no casting and makes logical sense:
string foo = "foo";
object o = foo; //you can assign an string to an object
A contravariant conversion is the opposite, and assignment requires a cast (which could throw an exception if it can’t be converted):
object foo = "foo";
string s = (string)foo;
Enter Collections
Though it’s obvious that you can convert a string into an object, shouldn’t you also be able to do this?
var strings = new List<string>();
List<object> objects = strings;
The short answer is no. Though converting from string to object is covariant, converting from a List of strings to a List of objects is not. A cast won’t compile either.
Want to see why? You can actually try it for yourself with an array, because an array is covariant (considered by some to be a mistake):
string[] strings = new string[2];
strings[0] = "foo";
object[] objects = strings;
objects[1] = new Object(); //things go BOOM.
This throws an OverflowException because you just tried to put an object into an array of strings. So, when Microsoft implemented generic lists, they didn’t allow you to expose yourself to this mistake, and they catch it in the compiler. That’s where covariance gets tricky.
In fact, contravariance gets tricky as well. To steal part of Eric Lippert’s example here, shouldn’t this compile? (Assuming Giraffe inherits from Animal)
void Foo(Giraffe g) {...}
Func<Animal> action1 = Foo;
This code is trying to make the assertion that the Foo method, which accepts a Giraffe as a parameter implements a delegate which accepts an Animal as a parameter. The problem is, if you actually called action1 with an Animal aside from a Giraffe, Foo wouldn’t be able to handle it and would produce an exception.
But, aren’t there other cases that don’t cause problems?
There are, which is why a number of changes were made to C# 4.0.
In our list/array example… what if you never added elements to the list? Wouldn’t the covariant conversion be safe? You’d never be able to add an object to an array of strings. The answer is yes, which is why the IEnumerable interface is marked covariant. This is perfectly valid (same example as above, swapping out List for IEnumerable):
var strings = new IEnumerable<string>();
IEnumerable<object> objects = strings;
IEnumerable is now considered covariant since it’s interface only exposes methods related to reading.
Similarly useful capabilities were added with regard to contravariance (example taken from here):
static string GetString() { return ""; }
Func
Func
The last line used to throw an exception, however this is actually a safe operation – any caller expecting an object to be returned will also be able to handle a string.