Invariance is a concept that can be seen in almost all the programming
languages that have a type system. And before we delve in to more language
specific constructs, let’s see what all this big words mean when it comes to a
language.
Covariance is the
ability to convert data from wider to narrow data types. (E.g. long to int).
Contra variance is
the ability to convert data from narrow 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 in between the lines you’ll
see that covariance preserves assignment
compatibility while contra variance reverses it, and when both covariance and
contra variance work together in tandem they make sure that invariance is avoided
when needed.
Ok, with that theory part covered let’s look at an example,
where which can expose 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
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 class BasicStorage 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.
In the above code 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 above code 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.
Hmm…
Although it makes sense to convert a string to an
object the .net framework is throwing 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, all objects are not strings.
If the above restriction was not applied by the
framework you would have been able to does something like this.
As you can see this code will try to store an ArrayList on
a memory location which 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 deferent perspective.
This time, let’s use two deferent interfaces to store
and retrieve data.
Now IDepostior
have the storage facility while IRetriever provides
us with reading functionality
The code that uses this new structure will look like
this.
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?
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 IRetriever interface only provides you with means to read
data, and 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 to the compiler that some implicit conversions
are legal and type safety does not have 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.
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 which
allows us to store data. Will this be ok?
Storage<object> inventory03 = new
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…), type “B” must also expose the same set of members too.
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 which will be carried out on the
generic typed items will only be operations which 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.
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”
key word, we’ve had made sure that the needed restrictions are there to make our
assignment type safe.
With that, we have just used Contra variance to reverse Covariance and made classes in an
inheritance hierarchy reference each other when the reference is type safe.
The completed interfaces will be…
If you have any questions, just add a comment and I’ll
answer it as soon as I can.
Hope this clears thing up.