Variance with Generic Inputs and Outputs
Although I could say that I understand covariance and contravariance, I still need to pause for a moment and think it over whenever I have to deal with them. In simple terms, I usually explain them as follows:
- Covariance allows a generic type to be assigned to a variable with a less specific generic type argument. Hence, the generic type parameter can only be in the role of a return value, and is marked with the
out
keyword. - Contravariance allows a generic type to be assigned to a variable with a more specific generic type argument. Hence, the generic type parameter can only be in the role of an input parameter, and is marked with the
in
keyword.
The following table list all tke key facts:
Variance type | Type arguments | Type parameter role | Keyword | Example |
---|---|---|---|---|
covariance | less specific | return value | out |
Func<T> |
contravariance | more specific | input parameter | in |
Predicate<T> |
It all gets even more complicated if the input parameters and return values in such a generic type are also generic types, i.e. the generic type parameter of the variant type is not directly in the role of the input parameter or the return value. Instead, the input parameters and return values are also variant generic types with the same type parameters as their generic type parameters. It's best, I explain this with code.
Let's start with a covariant type:
public interface ICovariant<out T>
{
// direct
T Get(); // as return value
// not allowed as input parameter
// bool Test(T input);
// indirect return value
Func<T> GetCovariant(); // in covariant type
// not allowed in contravariant type
// Predicate<T> GetContravariant();
// indirect input parameter
bool TestContravariant(Predicate<T> input); // in contravariant type
// not allowed in covariant type
// bool TestCovariant(Func<T> input);
}
To sum it up:
- As we already know, the generic type argument can directly only be in the role of a return value.
- When used as a generic type argument in a covariant type, that type can also only be in the role of a return value.
- However, when used as a generic type argument in a contravariant type, that type must be in the role of an input parameter.
Other combinations are not allowed and won't compile. We can explain all this if we look at some sample code for consuming this type. I will instantiate Covariant<T>
which implements the ICovariant<T>
interface:
ICovariant<Rectangle> ofRectangle = new Covariant<Rectangle>();
// assign to a variable with less specific generic type argument
ICovariant<Shape> ofShape = ofRectangle;
// function will return Rectangle
Func<Shape> func = ofShape.GetCovariant();
// not allowed: predicate would expect a Rectangle
// Predicate<Shape> predicate = ofShape.GetContravariant();
// predicate will receive a Rectangle
bool isPredicate = ofShape.TestContravariant(shape => true);
// not allowed: method would expect a Rectangle
// bool isFunc = ofShape.TestCovariant(() => new Shape());
As you can probably guess, Rectangle
is a class derived from Shape
. Let's think through all the combinations:
- Covariant return value is allowed because the
GetCovariant
method will return a function returning aRectangle
which will satisfy the consumer expecting a function returning aShape
. - Contravariant return value is not allowed because the
GetContravariant
method would return a predicate expecting aRectangle
, but the consumer could invoke it with aShape
. - Contravariant input parameter is allowed because the
TestContravariant
method will pass aRectangle
to the predicate which will satisfy the received predicate expecting aShape
. - Covariant input parameter is not allowed because the
TestCovariant
method would expect the function to return aRectangle
, but the received function could return aShape
.
Let's now move on to a contravariant type:
public interface IContravariant<in T>
{
// direct
bool Test(T input); // as input parameter
// not allowed as return value
// T Get();
// indirect input parameter
bool TestCovariant(Func<T> input); // in covariant type
// not allowed in contravariant type
// bool TestContravariant(Predicate<T> input);
// indirect return value
Predicate<T> GetContravariant(); // in contravariant type
// not allowed in covariant type
// Func<T> GetCovariant();
}
To sum it up:
- As we already know, the generic type argument can directly only be in the role of an input parameter.
- When used as a generic type argument in a covariant type, that type can also only be in the role of an input parameter.
- However, when used as a generic type argument in a contravariant tyoe, that type must be in the role of a return value.
Again, other combinations are not allowed and won't compile. We'll use similar sample code to explain all this. This time, I will instantiate Contravariant<T>
which implements the IContravariant<T>
interface:
IContravariant<Rectangle> ofRectangle = new Contravariant<Rectangle>();
// assign to a variable with more specific generic type argument
IContravariant<Square> ofSquare = ofRectangle;
// predicate expects a Rectangle
Predicate<Square> predicate = ofSquare.GetContravariant();
// not allowed: function will return Rectangle
// Func<Square> func = ofSquare.GetCovariant();
// method expects a Rectangle
bool isFunc = ofSquare.TestCovariant(() => new Square());
// not allowed: predicate would receive a Rectangle
// bool isPredicate = ofSquare.TestContravariant(square => true);
In this sample, Square
is a class derived from Rectangle
. Here are all the combinations explained:
- Contravariant return value is allowed because the
GetContravariant
method will return a predicate expecting aRectangle
which will be satisfied by theSquare
passed to it. - Covariant return value is not allowed because the
GetCovariant
method would return a function returning aRectangle
, but the consumer would expectit to return aSquare
. - Covariant input parameter is allowed because the
TestCovariant
method will expect a function returning aRectangle
and will be satisfied with the function returning aSquare
. - Contravariant input parameter is not allowed because the
TestContravariant
method could pass aRectangle
to the received predicate which would expect to receive aSquare
.
As everything with covariance and contravariance, some thinking is required to fully comprehend the details. Fortunately, most of the time the information listed in the following table will be everything you need:
Containing type variance | Covariant generic type | Contravariant generic type |
---|---|---|
covariance | as return value | as input parameter |
contravariance | as input parameter | as return value |
When that's not enough, feel free to re-read this post.