Tackling Invariance Using Covariance and Contravariance in C#

Invariance is a concept that can be seen in nearly all programming languages that have a type system. And before we delve into more of the language specific constructs, let's see what all these big words mean in the context of computer languages.

  • Covariance is the ability to convert data from a wider to a narrower data type. (E.g. long to int).
  • Contravariance is the ability to convert data from a narrower to wider data types. (E.g. int to float).
  • Invariance is the inability to do conversion between data types.

In other words if we read closely between the lines you'll see that covariance preserves assignment compatibility while contravariance reverses it, and when both covariance and contravariance work together in tandem they ensure that invariance is avoided when needed.

Ok, with that theory part covered let's look at an example that shows the invariant behavior of C#. And the example I'll be using will be a scenario where we will implement a generic interface for an imaginary inventory.

Let's have a look at the code first.

Contra-variance-in-CSharp1.gif

The IStorage interface defines two generic methods that facilitate storing and retrieving items from a generic List, and as you can see since there are no restrictions applied for type "T" in the implementing classBasicStorage or in the interface, any kind of data can be stored in our List repository. Let's write some code to use this inventory of ours.

Contra-variance-in-CSharp2.gif

In the code above I'm using the generic interface as the base type to access the implemented functionality because the class has implemented the methods explicitly in its code.

And when the code above is written all will work fine just the way we planned.

Ok, now what will happen if we try to store data of type object in our repository, to do that we will have to convert our inventory01 variable to an instance of type IStorage<object>.

And since all strings are objects and object is the base type for all types we should be able to do just that in our code. So let's write some code to do that.

Contra-variance-in-CSharp3.gif

Although it makes sense to convert a string to an object the .Net framework is throws an error when we try to do that, but why?

If you look at it closely you will see that although all strings are objects, the converse is not true, not all objects are strings.

If the preceding restriction was not applied by the framework you would have been able to does something like this.

Contra-variance-in-CSharp4.gif

As you can see this code will try to store an ArrayList in a memory location that is structured to store a string, and it will clearly mess up the type safety of the .Net framework.

This is the invariant behavior of .Net. C# adds invariance to the type system to ensure type safety in our programming constructs.

According to the language specification, in C# Generic Interfaces are Invariant by default while Generic Classes are always Invariant.

With that in mind, let's have a look at the same example from a different perspective.

This time, let's use two different interfaces to store and retrieve data.

Contra-variance-in-CSharp5.gif

Now IDepostior has the storage facility while IRetriever provides us with reading functionality.

The code that uses this new structure will look like this.

Contra-variance-in-CSharp6.gif

Here we cast an instance of "Storage<string> inventory02" to type "IDepostior<string> store" to store data, while we cast the same "inventory02" object to "IRetriever<string> retriever" to retrieve data.

With that said, will the following line of code work now?

Storage<string> inventory02new Storage<string>();
IRetriever<string> retreiver = inventory02;
IRetriever<object> objectRetreivor = inventory02;

The answer is "no".

In the previous example it made sense. Does it make the same sense now?

Here the IRetriever interface only provides you with a means to read data, it doesn't expose any method to store data. Because of that, you cannot store incompatible types in the underlying storage now. So the loop hole that allowed the previous example to break the type safety is no longer present in this interface. In other words, now it's perfectly safe to convert IRetriever<string> to IRetriever<object>.

In situations like this, where the type parameter only acts as a return value of a method in a generic interface, we can tell the compiler that some implicit conversions are legal and type safety does not need to be imposed on a type all the time. To do that we can use the "out" keyword.

And the modified IRetriever interface will look something like this.

Contra-variance-in-CSharp7.gif
Now we have just used Covariance to preserve assignment compatibility where it makes sense.

Right! Now let's have a look at the other interface that allows us to store data. Will this be ok?

Storage<object> inventory03new Storage<object>();
IDepostior<string> depositor = inventory03;

Let's think of it like this. All strings are objects, so if you can perform a specific behavior on a variable of type object, you should be able to carry out the same thing on a variable of type string.

In other words, if "B" derives from "A" and if type "A" exposes a set of members (behaviors, properties, etc…) then type "B" must also expose the same set of members.

So anything that can be carried out on A must also be supported by B.

So if we consider our IDepostior example, if we can store objects and do some exercises on them, we should be able to store strings and do the same set of exercise on them too.

That sounds correct, but there must be a way for us to tell to the complier that operations that will be carried out on the generic typed items will only be operations that are specified on the most generalized type in the inheritance hierarchy. Or in other words only the operations supported by objects will be carried out on our type.

We do that using the keyword "in".

The "in" keyword tells the compiler that, you can either pass type "T" as a parameter type to methods or pass any type that derives from "T". And you cannot use "T" as a return type of a method.

So the modified interface will look something like this.
Contra-variance-in-CSharp8.gif
Now we can reference an object either through a generic interface based on the object type or through a type that derived from that object type. And by adding the "in" keyword, we've ensured that the necessary restrictions exist to make our assignment type safe.

With that, we have just used Contravariance to reverse Covariance and made classes in an inheritance hierarchy reference each other when the reference is type safe.

The completed interfaces will be:

Contra-variance-in-CSharp9.gif

If you have any questions, just add a comment and I'll answer it as soon as I can.

Hope this clears thing up.

Up Next
    Ebook Download
    View all
    Learn
    View all