Overview
As object-oriented programmers we're all familiar with the concepts of covariance and contravariance, however not all programmers are comfortable with such industry-standard terms.
Let's review what they stand for. Take a look at the following code fragment:
class Vehicle
{
}
class Car : Vehicle //a derived class
{
}
void StartEngine( Vehicle v ); //simple method
Contravariance
Sure you know we may be calling the method StartEngine( ) by providing some instances of Car class. Well, it does make sense as we expect compiler implicitly up-cast the Car instances to Vehicle and then invoking the StartEngine( ) method. Yes, this very simple object-oriented technique is called "Contravariance." Contravariance is supported in almost all object-oriented programming languages. In C# 2.0, delegates support contravariance so that programmers see delegate invocations a bit more like normal method calls. Honestly, I think C# designers should had done this by the first version of the compiler, especially considering their implementation didn't require atypical CLR support.
Covariance
And take a look at this method: Vehicle GetMostExpensive( );
It also does make sense for C# programmers to return an instance of Car as the return value of this method. This is simply called "Covariance." Covariance is supported in most OOP languages. In C# 2.0, covariance is supported by delegates in the same way we expect from normal method calls. Again, it could had been available by the first version of C# compiler.
Covariant Arrays
The good news is that C# supports covariant arrays of reference-types. Here we go:
Car[ ] cars = new Car[ 9 ]; //an array of cars
Vehicle[ ] vehicles = cars; //super-type array
object[ ] vehicleObjects = cars; //object array!
Interesting point is that such type casting is 100% supported by CLR and therefore the resulting bits of compilation (.dll or .exe) do not include overheads. However, based on OOP constraints, the real instantiated type of the array is recognised by CLR and prevents adding an instance of non-expected type (String class for instance) to the vehicleObjects array - well, vehicleObjects is a reference which refers to an instance of type Car[ ]. Such attempts result into run-time errors.
But consider how the following code fails during compilation:
int[ ] intArray = new int[ 9 ]; //value-type array
object[ ] objectArray = intArray; //ERROR!
Although CLR supports direct covariant array type casting of reference-types, it doesn't support such behaviour for array of value-types such as int[ ] and DateTime[ ]. Here, I agree with CLR designers who decided not to support such feature as it has conflicts with inline arrays where such simple array casts toggle unintended memory block relocations and new array creation. I personally believe performance and security issues MUST always be visible to developers minimising human mistakes. But the main reason would probably be such casts result into new references which kicks out the whole idea. Not to mention such casts of int[ ] to object[ ] need all members be boxed to their equivalent object reference at first.
Other Covariant Collections
Do you expect the following line of code compile?
List<Car> cars = new List<Car>( ); //list of cars
List<Vehicle> vehicles = cars; //convert!
Nope, it doesn't compile! In fact, there are much differences between a simple array and an instance of List class. Array is directly supported by CLR whilst List is a custom class from CLR's point of view. There are hefty of custom collection classes available out there, and in this case, they all perform the same as .NET Framework's List class does.
Such collection classes (including List class) directly/indirectly rely on CLR's array type and usually contain instance(s) of this primitive array type. Do you really expect List<Car> be implicitly cast to List<Vehicle> as we did by primitive array in which no wrapper class was instantiated? If "yes", How about FileLogger<Car> and FileLogger<Vehicle>, do you still like that cast happen? There are many scenarios where such casts are making you crazy. Simply X<Car> is not of type X<Vehicle> and hence should not be cast!
But still you may realise there are scenarios in which consumers should be able to cast X<Car> to X<Vehicle>. Well, they're not casts in fact. They're simple conversions through which object references are not maintained. Such functionality can easily be supported by adding appropriate methods including conversion operators. However, C# compiler doesn't allow using implicit/explicit operators simulating array conversion (another wise decision), and implementers are to add some normal methods, say List<T>.ConvertTo<SuperType>( ).
Are you happy as implementers of framework's List class have already added ConvertAll( ) generic method?! Hey, don't let it fool you as it's a conversion method helping you "convert" individual entries of the original list to some other type enclosed in a new generic list - it's not a cast-like conversion at all! Actually framework's List implementers haven't added such method, in the sake of complexity it involves for both implementer and consumer developers. As a class library design best practice, it's usually not a good idea having read-write collection wrapper classes for casting purposes! Take a look at code below:
Car[ ] cars = new Car[ 9 ];
Vehicle[ ] vehicles = cars; //implicit cast
if( cars == vehicles ) { } //it's true
List<Car> cars = new List<Car>( );
List<Vehicle> vehicles = cars.UpCast<Vehicle>( );
if( cars == vehicles ) { } //it's FALSE!!!
As you see we've got a conversion (by internal wrapper classes) in which object references are not maintained, however internal primitive arrays are the same CLR objects:
List<
Car> cars =
new List<
Car>( );
List<
Vehicle> vehicles = cars.UpCast<
Vehicle>( );
if( cars[ 2 ] == vehicles[ 2 ] ) { }
//it's true!!! Though, it seems a missing workaround for the casting problem, it was sound decision not to add such method to List, as it brings further complexities to the code and eventually introduces complicated bugs. At the other hand, UpCast( ) method should return an internal wrapper object (either same or derived type) which delegates all operations to the original instance yet adding performance issues behind the scene in more complicated cases like this:
Dictionary<List<List<Car>>,List<List<String>>>
When converting to:
Dictionary<List<List<Vehicle>>,List<List<object>>>
Summary
- Casting and conversion are different as in cast object references are maintained. In other words, when casting no object creation happens but a new view to the existing object is maintained.
- C# language designers have not failed not supporting casting from GenericType<B> to GenericType<A> as this is wrong from OOP's point of view
- Primitive array cast is supported by both CLR and C#
e.g.: Car[ ] can be cast to Vehicle[ ] - all the references are maintained and no object constructions occur - C# implicit/explicit operators cannot be used to simulate cast-like conversion from same inheritance hierarchy path
- .NET Framework designers have not failed not adding cast-like conversion methods to some classes such as List as it has few performance and maintainability consequences. But still more advanced developers could be demanding this from their framework for certain scenarios.
Labels: .NET, C-Sharp, CLR, Contravariance, Conversion, Covariance