Today I took a look at the new covariance and contravariance features of .NET 4. In this post I will explain the theoretical background and will provide some practical examples.
The code snippets cover most of the features of CLR 1 to CLR 4.
Theoretical background
Covariance and contravariance are terms known from category theory. A covariant functor is a morphism between two categories, which preserves the structure.
If we use numbers as categories and numeric ordering as structure, the function f(x) = x + 10 preserves the ordering and therefore is covariant. The function f(x) = -x reverses the ordering and thus is contravariant.
Examples
In my examples I use the following class hierarchy:
Array covariance
The transformation is T -> T[], the structure is 'assignment compatibility'. If 'assignment compatibility' is preserved the transformation is covariant.
Since every Car is a Vehicle, we can assign a Car to a Vehicle variable. The same is true for arrays of Cars/Vehicles, so arrays are covariant.
Vehicle vehicle = new Car(); Vehicle[] vehicles = new Car[] { };
Array covariance only works for reference types. Arrays of value types are invariant:
object time = new DateTime();// Does not compile: Cannot convert from value type array to object array object[] times = new DateTime[] ;
Delegate covariance
Delegates are covariant in their return type. Image the following delegates:
delegate Vehicle VehicleFactoryDelegate(); delegate Car CarFactoryDelegate();
The VehicleFactoryDelegate produces Vehicles. Since every Car is a Vehicle, we can assign a CarFactoryDelegate to a VehicleFactoryDelegate:
VehicleFactoryDelegate vehicleFactory = CreateVehicle; CarFactoryDelegate carFactory = CreateCar; vehicleFactory = CreateCar; // Does not compile: vehicleFactory produces vehicles, but carFactory only produces cars carFactory = CreateVehicle;
Delegate contravariance
Delegates are contravariant in their parameter types. Image the following delegates:
delegate void VehicleProcessorDelegate(Vehicle vehicle); delegate void CarProcessorDelegate(Car car);
The CarProcessorDelegate processes Cars. Since every Car is a Vehicle, we can assign a VehicleProcessorDelegate to a CarProcessorDelegate:
VehicleProcessorDelegate vehicleProcessor = ProcessVehicle; CarProcessorDelegate carProcessor = ProcessCar; carProcessor = ProcessVehicle; // Does not compile: carProcessor can only work with cars, but vehicleProcessor is able to process vehicles vehicleProcessor = ProcessCar;
.NET 4.0: Generics covariance
With .NET two new keywords out/in where introduced. They can be used to explicitly enable co-/contravariance for generic type parameters. Out is used to enable covariance in the returned type (note the out-keyword in System.Func<out TResult>):
Func<Vehicle> vehicleFactory = () => new Vehicle(); Func<Car> carFactory = () => new Car(); // Only CLR 4 vehicleFactory = carFactory; // Does not compile: vehicleFactory produces vehicles, but carFactory only produces cars carFactory = vehicleFactory;
Use the out-keyword in generic interfaces to enable covariance:
interface ICovariant<out T> { T Create(); }
.NET 4.0: Generics contravariance
In is used to enable contravariance in parameter types (note the in-keyword in System.Action<in T>):
Action<Vehicle> vehicleProcessor = v => { }; Action<Car> carProcessor = c => { }; // Only CLR 4 carProcessor = vehicleProcessor; // Does not compile: carProcessor can only work with cars, but vehicleProcessor is able to process vehicles vehicleProcessor = carProcessor;
Use the in-keyword in generic interfaces to enable contravariance:
interface IContravariant<in T> { void Process(T t); }