Variance with Generic Inputs and Outputs

January 4th 2019 C#

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 a Rectangle which will satisfy the consumer expecting a function returning a Shape.
  • Contravariant return value is not allowed because the GetContravariant method would return a predicate expecting a Rectangle, but the consumer could invoke it with a Shape.
  • Contravariant input parameter is allowed because the TestContravariant method will pass a Rectangle to the predicate which will satisfy the received predicate expecting a Shape.
  • Covariant input parameter is not allowed because the TestCovariant method would expect the function to return a Rectangle, but the received function could return a Shape.

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 a Rectangle which will be satisfied by the Square passed to it.
  • Covariant return value is not allowed because the GetCovariant method would return a function returning a Rectangle, but the consumer would expectit to return a Square.
  • Covariant input parameter is allowed because the TestCovariant method will expect a function returning a Rectangle and will be satisfied with the function returning a Square.
  • Contravariant input parameter is not allowed because the TestContravariant method could pass a Rectangle to the received predicate which would expect to receive a Square.

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.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License