Daily Knowledge Drop
The way in which base and inherited classes can automatically be cast up or down the hierarchy (depending on the situation) is referred to covariance
and contravariance
.
Most developers have probably used the concepts of covariance
and contravariance
in their code, perhaps without even realising it. Looking at a some examples, will help explain in a bit more detail.
Base setup
In the examples below, the following hierarchy of classes is used:
public class Vehicle { }
public class LandVehicle : Vehicle { }
public class SeaVehicle : Vehicle { }
public class Car : LandVehicle { }
public class Boat : SeaVehicle { }
Contravariance
Contravariance
applies to types going in, in other words parameters to methods
.
Contravariance
allows for methods with a parameter of a base class, to accept any type derived from the base class
.
Summary: Contravariance
=> IN
=> parameter declared as base, but derived can be used
An example:
// declare a lambda function which takes in a Vehicle
var func = (Vehicle veh) => Console.WriteLine(veh.ToString());
// the function will accept all types of vehicles
func(new Vehicle());
func(new Car());
func(new SeaVehicle());
The lambda is declared with a parameter of Vehicle, but any type of derived vehicle will be accepted.
Because the lambda takes in a Vehicle if there is a method or property specific to a child which needs to be invoked, the Vehicle type needs to be checked and downcast the derived type
// the function will accept all types of vehicles
OuputExtendedDetails(new Vehicle());
OuputExtendedDetails(new Car());
OuputExtendedDetails(new SeaVehicle());
// parameter of type Vehicle
public void OuputExtendedDetails(Vehicle veh)
{
// each IF statement will try downcast and assign to the variable (lv in below case)
// if allowed to do so
if(veh is LandVehicle lv)
{
Console.WriteLine($"{lv.GetType()} travels on land");
}
if (veh is Car car)
{
Console.WriteLine($"{car.GetType()} travels on land");
}
if (veh is SeaVehicle sv)
{
Console.WriteLine($"{sv.GetType()} travels on sea");
}
if (veh is Boat boat)
{
Console.WriteLine($"{boat.GetType()} travels on sea");
}
Console.WriteLine("Vehicle can travel");
}
The output is as follows:
Vehicle can travel
Car travels on land
Car travels on land
Vehicle can travel
SeaVehicle travels on sea
Vehicle can travel
A Car for instance:
- is a Vehicle so can be passed into the method
- is a LandVehicle so ouput is written
- is a Car so ouput is written
In short, this is contravariance
- the ability to use a derived class as a parameter, where a base class has been specified
.
Covariance
Covariance
applies to types coming out, in other words return types from methods
or assignments.
Covariance
allows for passing back a derived type where a base type is expected
.
Summary: Covariance
=> OUT
=> type declared as derived, but base can be used
// Even though a Car and Boat
// are being returned, they are assigned to Vehicle
Vehicle car = GetCar();
Vehicle boat = GetBoat();
// get a Car
public Car GetCar()
{
return new Car();
}
// get a Boat
public Boat GetBoat()
{
return new Boat();
}
Here, the return types are Car and Boat, but they can both be assigned to a variable of type Vehicle.
This is useful, for example, when we want to have a list of Vehicles. The list is declared of type Vehicle, and therefor can hold any type of vehicle:
// add a Car and Boat to a Vehicle list
var vehList = new List<Vehicle>();
vehList.Add(GetCar());
vehList.Add(GetBoat());
// output the items
foreach(var veh in vehList)
{
Console.WriteLine(veh.ToString());
}
public Car GetCar()
{
return new Car();
}
public Boat GetBoat()
{
return new Boat();
}
The output is as follows:
Car
Boat
In short, this is covariance
- the ability to assign a derived type, to its base class
.
Contravariance - Generics
Contravariance
can also be applied to Generics, using the in
keyword.
Using the in
keyword allows for the usage of a less derived type than the one specified by the generic parameter
.
Consider the following setup (without the in keyword):
public interface ITravel<TVehicle> { }
public class Travel<TVehicle> : ITravel<TVehicle> { }
Using the above setup, the following will NOT compile:
// An instance of ITravel<Car> is declared and the
// MoveCar method is called without issue
var carTravel = new Travel<Car>();
MoveCar(carTravel);
// An instance of ITravel<LandVehicle> is declared and the the
// MoveCar method is tried to be called. This is NOT ALLOWED.
var landTravel = new Travel<LandVehicle>();
MoveCar(landTravel); // THIS IS NOT ALLOWED
// A method is declared which takes an instance of ITravel<Car>
public void MoveCar(ITravel<Car> travel)
{
Console.WriteLine(travel);
}
As the generic parameter is not declared with the in
keyword, it is not contravariant
. By just adding the in
keyword, the above will be allowed:
public interface ITravel<in TVehicle> { }
public class Travel<TVehicle> : ITravel<TVehicle> { }
Now, any type which is less derived than Car
can be used instead of Car. The below is now 100% valid and will compiled with any issue:
var carTravel = new Travel<Car>();
MoveCar(carTravel);
var landTravel = new Travel<LandVehicle>();
MoveCar(landTravel);
var vehTravel = new Travel<Vehicle>();
MoveCar(landTravel);
public void MoveCar(ITravel<Car> travel)
{
Console.WriteLine(travel);
}
The output:
Travel`1[Car]
Travel`1[LandVehicle]
Travel`1[Vehicle]
Covariance - Generics
Covariance
can also be applied in Generics, using the out
keyword.
Using the out
keyword allows for the usage of a more derived type than the one specified by the generic parameter
.
Consider the following setup (without the out keyword):
interface ITravel<TVehicle> { }
class Travel<TVehicle> : ITravel<TVehicle> { }
Using the above setup, the following will NOT compile:
ITravel<Vehicle> veh = new Travel<Vehicle>();
ITravel<Car> car = new Travel<Car>();
// THIS IS NOT ALLOWED
veh = car;
The instance of Travel<Car> (a more derived type) cannot cannot be assigned to a variable using type Vehicle (a less derived type).
As the generic parameter is not declared with the out
keyword, it is not covariant
. By just adding the out
keyword to the generic parameter, the above will be allowed:
interface ITravel<out TVehicle> { }
class Travel<TVehicle> : ITravel<TVehicle> { }
Now, any type which is a more derived type than Vehicle
can be used instead of Vehicle. The below is now 100% valid and will compiled with any issue:
ITravel<Vehicle> veh = new Travel<Vehicle>();
ITravel<Car> car = new Travel<Car>();
ITravel<SeaVehicle> sea = new Travel<SeaVehicle>();
Console.WriteLine(veh);
Console.WriteLine(car);
veh = car;
Console.WriteLine(veh);
veh = sea;
Console.WriteLine(veh);
The output:
Travel`1[Vehicle]
Travel`1[Car]
Travel`1[Car]
Travel`1[SeaVehicle]
IEnumerable - Generics
The above examples, while doing a satisfactory job in demonstrating the functionality, are not concrete examples.
So lets have a quick look at the .NET IEnumerable<> class, which is defined with the out
keyword:
namespace System.Collections.Generic
{
public interface IEnumerable<out T> : IEnumerable
{
// removed for brevity
}
}
This allows IEnumerable<> to be used as follows:
IEnumerable<Car> cars = new List<Car>();
IEnumerable<Vehicle> vehicle = cars;
This is covariance
in action - more derived type
can be used and assigned to a less derived type
.
Notes
In summary:
Contravariance
=>in
=>parameter declared as base, but derived can be used
Covariance
=>out
=>type declared as derived, but base can be used
Knowing about covariance
and contravariance
, especially when it comes to generics, is useful to know and can be leveraged to reduce duplicate code.
References
Out (generic modified)
In (generic modified)
Covariance and Contravariance in C# Explained
Daily Drop 34: 18-03-2022
At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.
The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.