Contravariance – Architectural Principles

To demo covariance, we leverage the following generic interface:
public interface IContravariant
{
void Set(T value);
}

In C#, the in modifier, the highlighted code, explicitly specifies that the generic parameter T is contravariant. Contravariance applies to input types, hence the Set method that takes the generic type T as a parameter.Before testing this out, we need an implementation. Here’s a barebone one:
public class WeaponSetter : IContravariant
{
private Weapon?
_weapon;
public void Set(Weapon value)
=> _weapon = value;
}

The highlighted code, which represents the T parameter, is of type Weapon, the topmost class in our model; other classes derive from it. Since contravariance means you can input the instance of a subtype as its supertype, using the Weapon supertype allows exploring this with the Sword and TwoHandedSword subtypes. Here’s the xUnit fact that demonstrates contravariance:
[Fact]
public void Generic_Contravariance_tests()
{
IContravariant weaponSetter = new WeaponSetter();
IContravariant swordSetter = weaponSetter;
Assert.Same(swordSetter, weaponSetter);
// Contravariance: Weapon > Sword > TwoHandedSword
weaponSetter.Set(new Weapon());
weaponSetter.Set(new Sword());
weaponSetter.Set(new TwoHandedSword());
// Contravariance: Sword > TwoHandedSword
swordSetter.Set(new Sword());
swordSetter.Set(new TwoHandedSword());
}

The highlighted line represents contravariance. We can implicitly convert the IContravariant supertype to the IContravariant subtype.The code after that showcases what happens with that polymorphic change. For example, the Set method of the weaponSetter object can take a Weapon, a Sword , or a TwoHandedSword instance because they are all subtypes of the Weapon type (or is the Weapon type itself).The same happens with the swordSetter instance, but it only takes a Sword or a TwoHandedSword instance, starting at the Sword type in the inheritance hierarchy because the compiler considers the swordSetter instance to be an IContravariant, even if the underlying implementation is of the WeaponSetter type.Writing the following yields a compiler error:
swordSetter.Set(new Weapon());

The error is:
Cannot convert from Variance.Weapon to Variance.Sword.

That means that for the compiler, swordSetter is of type IContravariant, not IContravariant.
Note
I left a link in the Further reading section that explains covariance and contravariance if you want to know more since we just covered the basics here.
Now that we grazed covariance and contravariance, we are ready to explore the formal version of the LSP.

You may also like