Co and contravariance in C#
April 18, 2018Introduction
This article is written mostly for me. For a long time, I thought that I understand the topic. It was only after one of the job interviews I realised that I did not grasp the concept and found it hard to remember the difference between co and contravariance.
Generic variance is a property of relation between generic type and its type parameter. It describes how the relationship between generic types is influenced by the relation of its type parameters.
There are three types of the generic variance in c#:
- Invariant
- Covariant
- Contravariant
By default, generic types are invariant. Covariant type parameters are denoted with the out
clause while contravariant type parameters are marked with the in
clause. The meaning of those clauses will be clear shortly.
Covariance and contravariance have very interesting property. They restrict the usage of type parameter for the generic type while simultaneously expanding available usage of the generic type itself. In that sense, they are similar to constraints on type parameters.
Code workbench
The following code will be the base for all further examples. Properties of each variance type will be presented as standalone tests.
public class Document {}
public class InvoiceDocument : Document {}
public interface IGenericWithInvariantParam<T>
{
T Prop { get; set; }
}
public interface IGenericWithCovariantParam<out T>
{
T Prop { get;}
}
public interface IGenericWithContravariantParam<in T>
{
T Prop { set; }
}
Invariance
By default, all generic types are invariant. Objects of generic types with different type parameters cannot be assigned to each other.
[Test]
public void GenericWithInvariantTypeIsNotAssignableToBase()
{
var baseGenericType =
typeof(IGenericWithInvariantParam<Document>);
var descendantGenericType =
typeof(IGenericWithInvariantParam<InvoiceDocument>);
descendantGenericType.Should().NotBeAssignableTo(baseGenericType);
}
[Test]
public void GenericWithInvariantTypeIsNotAssignableToDescendant()
{
var baseGenericType =
typeof(IGenericWithInvariantParam<Document>);
var descendantGenericType =
typeof(IGenericWithInvariantParam<InvoiceDocument>);
baseGenericType.Should().NotBeAssignableTo(descendantGenericType);
}
Covariance
Out of two variances covariance is the one easier to grasp, because it is quite intuitive. Covariant generic types resemble the relation of its type parameters. If type parameter can be assigned to another type parameter, so can generic types. If InvoiceDocument
can be assigned to Document
therefore IGenericWithCovariantParam<InvoiceDocument>
can be assigned to IGenericWithCovariantParam<Document>
.
[Test]
public void GenericWithCovariantTypeIsAssignableToBase()
{
var baseGenericType =
typeof(IGenericWithCovariantParam<Document>);
var descendantGenericType =
typeof(IGenericWithCovariantParam<InvoiceDocument>);
descendantGenericType.Should().BeAssignableTo(baseGenericType);
}
[Test]
public void GenericWithCovariantTypeIsNotAssignableToDescendant()
{
var baseGenericType =
typeof(IGenericWithCovariantParam<Document>);
var descendantGenericType =
typeof(IGenericWithCovariantParam<InvoiceDocument>);
baseGenericType.Should().NotBeAssignableTo(descendantGenericType);
}
The clause out
means that type parameter can be used on the output positions of properties and methods, never on the input. The most common example of a covariant type is IEnumerable<out T>
. It can be declared as covariant because IEnumerable
is a read-only type so that T
is never on the input position. Conceptually it means that if you have an IEnumerable<Apple>
you can use it in place of IEnumerable<Fruit>
because Apple
is a Fruit
.
Contravariance
Contravariance is a mirror of variance. It is represented by the in
keyword, and it means that objects with the contravariant type parameter can be used only on the input positions like method arguments.
The most counter-intuitive property of contravariant generic types is the fact that relation between generic types with different type parameters is reversed. Following tests display that property.
[Test]
public void GenericWithContravariantTypeIsNotAssignableToBase()
{
var baseGenericType =
typeof(IGenericWithContravariantParam<Document>);
var descendantGenericType =
typeof(IGenericWithContravariantParam<InvoiceDocument>);
descendantGenericType.Should().NotBeAssignableTo(baseGenericType);
}
[Test]
public void GenericWithContravariantTypeIsAssignableToDescendant()
{
var baseGenericType =
typeof(IGenericWithContravariantParam<Document>);
var descendantGenericType =
typeof(IGenericWithContravariantParam<InvoiceDocument>);
baseGenericType.Should().BeAssignableTo(descendantGenericType);
}
In the standard library, the most popular contravariant generic type is probably
public delegate void Action<in T>(T obj)
Summary
- Invariant - all generic types by default, no relation between generic types with different type parameters
- Covariant - Delegates and interfaces with
out
type parameters. Values of those types can be used only in the output positions, like method return values. - Contravariant - Delegates and interfaces with
in
type parameters. Values of those types can be used only in the input positions, like method arguments.