Generic variance in C#, part 2 – Contravariance

! Warning: this post hasn't been updated in over three years and so may contain out of date information.

csharp_genericsContravariance is perhaps the hardest of the three types of generic variance to understand (at least I found it so). This article hopefully takes the reader through two examples of its use to explain what it is, before showing how it’s used for real in the .NET Framework.

This is part 2 of a three part series on generic variance in C#. Part 1, covers invariance. The final part, covers covariance.

To begin explaining contravariance, let’s start with some code. It’s a simple program that emulates (in a very loose sense of the word) a dot matrix display panel. The class accepts a character and a display position, but in reality just writes to the console. Access to that panel is then via an implementation of IDisplay. The implementation sends a string, one character at a time to the panel, up to the NumberOfCharactersSupported. If the string is longer than that constant, it substitutes the last character written with an ellipsis (…).

In order to demonstrate why contravariance is important, the Main class does some contrived casting from IDisplay<object> to IDisplay<string> via two variables x and y.

The code itself is as follows:

If you paste the above code into Visual Studio or LINQPad and attempt to run it, it’ll give you an error: “CS0266 Cannot implicitly convert type 'IDisplay<object>' to 'IDisplay<string>'“. As discussed in part 1 of this series, generic classes are implicitly invariant: we can’t cast C<T> to C<SonOfT>, thus the error.

However, since x is an instance of a class that meets the IDisplay<object> contract, then it clearly can handle being given a string to handle, so there shouldn’t – in this case – be a problem with assigning it to y, which only needs something that can meet the IDisplay<string> contract. And this is where contravariance comes in to play. The reason why it can be treated as a safe cast is because IDisplay<T> only allows values of T to be passed in to it, not pulled out. Therefore casting from IDisplay<object> to IDisplay<string> wouldn’t cause a runtime failure if the cast were permitted. When T is only passed in, then if the code can handle T being object, then it can clearly handle it being string too, for the latter is a subtype of the former. The solution therefore is to reassure the compiler (and the CLR) that this is what we want to do: we add in to the interface declaration to commit to only ever passing instances of T into it, not out again:

If you apply this change to the original code, it will now compile and run correctly.

What actually is the point of this feature though, beyond my rather contrived example above? Well to properly explain that, let’s first wander even further into the land of highly contrived code:

If you work your way through the code, you’ll see that it first declares two interfaces, ISumCalculator<T> and IShape. ShapeAreaSumCalculator then implements ISumCalculator<IShape> to provide a means of adding two areas together. Circle is an implementation of IShape that correctly works out the area of a circle. Finally Main creates two circles, adds their areas and reports the result.

The reason for the rather odd line var z = Sum<Circle>(new ShapeAreaSumCalculator(), x, y); is to once more highlight an example where contravariance can come to our rescue. As the code currently stands, it won’t compile as ISumCalculator<T> is lacking the magic in parameter. Add that, and it will compile. That’s because we are asking the compiler to cast an implementation of ISumCalculator<IShape> to ISumCalculator<Circle>. We need to reassure the compiler that ShapeAreaSumCalculator will be able to handle the ISumCalculator<Circle> contract by making it contravariant. If I now create more shapes classes, Rectangle, Triangle and the like then, as long as they implement IShape, the ShapeAreaSumCalculator will be able to handle summing the area of any two of them (including, for example, summing the areas of a circle and triangle).

So far, I’ve shown a set of contrived examples to explain how contravariance works. Does it have any real-world uses though? The answer to that is that it’s used in a very similar way to ISumCalculator<T> in implementations of the IComparer<T>, IComparable<T>, and IEqualityComparer<T> interfaces. To see contravariance in action, consider the following slightly reworked example of using IComparer<T>.

If IComparer<T> weren’t contravariant, then rather than using ShapeAreaComparer for every implementation of IShape, we’d have to create a comparer for each type. That would then cause problems if we had eg SortedSet<IShape>, as we’d need a way to route the correct comparer through to Sort to enable that set to sort itself.

In summary, as long as a generic interface, IGeneric<T>, only has members that pass T in, then that interface can use in T to declare itself contravariant. Then any implementation of IGeneric<T1> can be used to handle T2, just so long as T2 is a subtype of T1.

And that is why contravariance exists. It’s an odd, edge-case feature, but it has its uses. Hopefully, if my explanation has made sense to you, it’s a feature that you may be able to use yourself from now on. If you are still somewhat confused, then my advice is to do what I did. I had to come up with examples of code that only compiled and functioned correctly when I used contravariance. Try it yourself and it may help you grasp the concept too.

Posted in C#