What is Covariance and Contravariance in C#?

09 juli 2021 om 10:00 by ParTech Media - Post a comment

When C# version 4.0 was released, two new capabilities were introduced to the language's already extensive range of tools for solving various problems. Yes, we are talking about the popular covariance and contravariance features.

This post will define and demonstrate how these two handy features that allow developers to solve challenges which previously required a workaround.

In simple words, the co and contra-variances refer to how an object is handled. The term "variance" refers to a property of operators that act on specified types, as well as the ordering of operators. When compared to its operands, the co prefix implies "along with," and indicates that the operator retains the ordering of types. When compared to its operands, the contra prefix implies "against '' something and indicates that the operator reverses the ordering of types.

On that note, let us understand in detail what is covariance and contravariance in C#.

Table of contents

  1. What is Covariance in C#?
  2. What is Contravariance in C#?
  3. Arrays Covariance
  4. Conclusion

What is Covariance in C#?

Let's start with a simple definition of covariance. Assuming that each animal has a name, we can construct a function that outputs the names of all animals in a collection:

It's worth noting that IEnumerable can only be used to read Animal values from the collection (through the Current property of IEnumerator). We won't be able to add Animal to the collection because it's read-only.

Now try answering an important question - Is it possible to use PrintAnimals to print the names of a collection of cats?

If you compile and run this sample, you'll notice that the C# the compiler recognizes it and that the application runs well. The compiler converts IEnumerable to IEnumerable by using covariance when calling PrintAnimals. This is true because the out annotation specifies that the IEnumerable interface is covariant.

The PrintAnimals method cannot go wrong when you run the application since it can only do one thing: print. Since it can only read animals from the collection, the PrintAnimals method cannot cause any problems when you run the program. So it's fine to use a collection of cats to make a point because all cats are animals.

What is Contravariance in C#?

Contravariance is exactly the oppposit of covariance. Let's imagine we have a method that creates some cats and compares them with an IComparer object that we provide. In a more practical scenario, the approach might sort the cats as follows:

Cats are passed as parameters to the comparer object, but it never returns a cat as a result. This is primarily because of the way it uses the generic type parameter - you could say it's write-only. Now, owing to contravariance, we can make a comparator that compares animals and pass it to CompareCats as an argument:

Since the IComparer interface is contravariant and the generic type parameter is indicated with the in annotation, the compiler accepts this code. We constructed a compareAnimals object that can compare animals. This means it can absolutely compare two cats.

Array Covariance

Have a look at the following method:

It takes an array of Person objects as input and outputs the names of all of them to the console. But what if we pass an array of Teacher or Student instead of a Person array as follows -

This code compiles, runs, and is perfectly logical. Because both Student and Teacher have names. Supplying Student[] or Teacher[] instead of Person[] makes our method far more useful than if we had provided an exact type match of Person[].

With this example in mind, you might conclude that we should be able to send an object with the generic parameter Teacher or Student everywhere we anticipate a generic type of Person. This is not true. Consider the following scenario:

What if we call this method like this -

This code compiles; however an exception will be thrown during runtime. We can reduce the example to explain the problem differently:

Pay close attention to the assignments. Student[] is the type of the original array. However, because we can replace Person[] with Student[], we receive a reference to an array of Person objects on line 6.

The compiler allows the execution of line 7 since every teacher is a person. However, this is where the problem occurs during runtime:

This is, of course, something we would expect. What would the assignment statement on line 8 do if this code had worked? We'd be attempting to pair a student with a teacher. That would be absurd, to say the least, because these two categories are incompatible.

Arrays are covariant if you can pass an array of Student or Teacher when an array of Person is anticipated. When the language didn't enable generic types, this was a deliberate decision from the start. Although there may be some potentially disastrous repercussions, as we've seen, this behavior provides a great deal of flexibility when it comes to implementing general-purpose algorithms, such as the PrintNames method.

Conclusion

Covariance and contravariance were added to the C# language so that polymorphism features could be added to arrays, delegate types, and generic types. You can use a less derived type than you initially specified with contravariance, and you can use a more derived type with covariance. Depending on the type of data you're working with, you can use covariance or contravariance.