Generic variance in C#, part 3 – Covariance

csharp_genericsIn part 2 of this series, we looked at contravariance, which is a special case of generic variance, for interfaces that only allow instances of T, for ISomeInterface<T>, to be passed in, either via method parameters or through property setters (though good code will not contain the latter, of course). It’s perhaps no surprise that covariance is the opposite. Only when ISomeInterface<T> allows instances of T to be passed out (either via method returns, or property getters), can that interface be covariant.

As per usual, let’s start with some contrived code:

Again, following the conventions of the previous parts of this series, if you paste the above code into Visual Studio or LINQPad, it won’t compile, with the line PrintTemperatures(sensors); resulting in the error
CS1503 Argument 1: cannot convert from 'TemperatureSensors' to 'ISensorCollection<ISensor>'. That’s because PrintTemperatures is expecting an instance of ISensorCollection<ISensor>. Again though, the problem is easily fixed. ISensorCollection<T> only passes out instances of T, so change it to:

and once more it will compile and run.

The reason behind the existence of the “reinvention of the wheel” interface, ISensorCollection<T> is because it highlights one of the key areas of functionality that make use of covariance: collections that are iterated over. Unsurprisingly, IEnumerable<T> is also covariant. We can use this to then simplify our code:

Wrap up

The following applies for types ISomeInterface<Base> and ISomeInterface<Derived>, where Derived is a subtype of Base:

  1. By default, generic types are invariant: you cannot cast between ISomeInterface<Base> and ISomeInterface<Derived>.
  2. If ISomeInterface<T> is contravariant, then it only allows instances of T to be passed in. In this case, an instance of ISomeInterface<Base> can be assigned to a variable or parameter of type ISomeInterface<Derived>, ie you can cast from ISomeInterface<Base> to ISomeInterface<Derived>. This because, as per normal inheritance rules, if it is expecting an instance of Base, then it can handle being given an instance of Derived.
  3. If ISomeInterface<T> is covariant, then it only allows instances of T to be passed out. In this case, an instance of ISomeInterface<Derived> can be assigned to a variable or parameter of type ISomeInterface<Base>, ie you can cast from ISomeInterface<Derived> to ISomeInterface<Base>. This is because, the receiving code is expecting a Base and so can accept being given a Derived.
Posted in C#