Covariance and Contravariance

A question often asked by new users of generic types is why an expression of type List<string> may not be assigned to a variable of type List<object>: If a string may be converted to type object, surely a list of strings is similarly compatible with a list of objects. In reality, this is not, generally speaking, either type-safe or legal. If you declare two variables with different type parameters using the same generic class, the variables are not type-compatible, even if they are assigning from a more specific type to a more generic type—in other words, they are not covariant.

Covariant is a technical term from category theory, but its underlying idea is straightforward: Suppose two types X and Y have a special relationship—namely, that every value of the type X may be converted to the type Y. If the types I<X> and I<Y> always also have that same special relationship, we say, “I<T> is covariant in T.” When dealing with simple generic types with only one type parameter, the type parameter can be understood such that we simply say, “I<T> is covariant.” The conversion from I<X> to I<Y> is called a covariant conversion.

For example, two instances of a generic class, Pair<Contact> and Pair<PdaItem>, are not type-compatible even when the type arguments are themselves compatible. In other words, the compiler prevents the conversion (implicit or explicit) of Pair<Contact> to Pair<PdaItem>, even though Contact derives from PdaItem. Similarly, converting Pair<Contact> to the interface type IPair<PdaItem> will fail. See Listing 12.40 for an example.

Listing 12.40: Conversion between Generics with Different Type Parameters
// ...
// Error: Cannot convert type ...
Pair<PdaItem> pair = (Pair<PdaItem>)new Pair<Contact>();
IPair<PdaItem> duple = (IPair<PdaItem>)new Pair<Contact>();

But why is this not legal? Why are List<T> and Pair<T> not covariant? Listing 12.41 shows what would happen if the C# language allowed unrestricted generic covariance.

Listing 12.41: Preventing Covariance Maintains Homogeneity
// ...
Contact contact1 = new("Princess Buttercup");
Contact contact2 = new("Inigo Montoya");
Pair<Contact> contacts = new(contact1, contact2);
 
// This gives an error: Cannot convert type ...
// But suppose it did not
IPair<PdaItem> pdaPair = (IPair<PdaItem>) contacts;
// This is perfectly legal, but not type-safe
pdaPair.First = new Address("123 Sesame Street");

An IPair<PdaItem> can contain an address, but the object is really a Pair<Contact> that can contain only contacts, not addresses. Type safety is completely violated if unrestricted generic covariance is allowed.

Now it should also be clear why a list of strings may not be used as a list of objects. You cannot insert an integer into a list of strings, but you can insert an integer into a list of objects; thus it must be illegal to cast a list of strings to a list of objects—an error the compiler can enforce.

Enabling Covariance with the out Type Parameter Modifier

You might have noticed that both problems described earlier as consequences of unrestricted covariance arise because the generic pair and the generic list allow their contents to be written. Suppose we eliminated this possibility by creating a read-only IReadOnlyPair<T> interface that exposes T only as coming “out” of the interface (i.e., used as the return type of a method or read-only property) and never going “into” it (i.e., used as a formal parameter or writeable property type). If we restricted ourselves to an “out-only” interface with respect to T, the covariance problem just described would not occur (see Listing 12.42).13

Listing 12.42: Possible Covariance
interface IReadOnlyPair<T>
{
    T First { get; }
    T Second { get; }
}
 
interface IPair<T>
{
    T First { getset; }
    T Second { getset; }
}
 
public struct Pair<T> : IPair<T>, IReadOnlyPair<T>
{
    // ...
}
 
public class Program
{
    static void Main()
    {
        // Only possible with out type parameter
        //Pair<Contact> contacts =
        //    new Pair<Contact>(
        //        new Contact("Princess Buttercup"),
        //        new Contact("Inigo Montoya"));
        //IReadOnlyPair<PdaItem> pair = contacts;
        //PdaItem pdaItem1 = pair.First;
        //PdaItem pdaItem2 = pair.Second;
    }
}

When we restrict the generic type declaration to expose data only as it comes out of the interface, there is no reason for the compiler to prevent covariance. All operations on an IReadOnlyPair<PdaItem> instance would convert Contacts (from the original Pair<Contact> object) up to the base class PdaItem—a perfectly valid conversion. There is no way to “write” an address into the object that is really a pair of contacts, because the interface does not expose any writeable properties.

The code in Listing 12.42 still does not compile.14 To indicate that a generic interface is intended to be covariant in one of its type parameters, you can declare the type parameter with the out type parameter modifier. Listing 12.43 shows how to modify the interface declaration to indicate that it should be allowed to be covariant.

Listing 12.43: Covariance Using the out Type Parameter Modifier
interface IReadOnlyPair<out T>
{
    T First { get; }
    T Second { get; }
}

Modifying the type parameter on the IReadOnlyPair<out T> interface with out will cause the compiler to verify that T is, indeed, used only for “outputs”—method return types and read-only property return types—and never for formal parameters or property setters. From then on, the compiler will allow any covariant conversions involving the interface to succeed. When this modification is made to the code in Listing 12.42, it will compile and execute successfully.

Several important restrictions are placed on covariant conversions:

Only generic interfaces and generic delegates (described in Chapter 13) may be covariant. Generic classes and structs are never covariant.
The varying type arguments of both the source and target generic types must be reference types, not value types. That is, an IReadOnlyPair<string> may be converted covariantly to IReadOnlyPair<object> because both string and IReadOnlyPair<object> are reference types. An IReadOnlyPair<int> may not be converted to IReadOnlyPair<object> because int is not a reference type.
The interface or delegate must be declared as supporting covariance, and the compiler must be able to verify that the annotated type parameters are, in fact, used in only “output” positions.
Enabling Contravariance with the in Type Parameter Modifier

Covariance that “goes backward” is called contravariance. Again, suppose two types X and Y are related such that every value of the type X may be converted to the type Y. If the types I<X> and I<Y> always have that same special relationship “backward”—that is, every value of the type I<Y> can be converted to the type I<X>—we say that “I<T> is contravariant in T.”

Most people find that contravariance is much harder to comprehend than covariance is. The canonical example of contravariance is a comparer. Suppose you have a derived type, Apple, and a base type, Fruit. Clearly, they have the special relationship: Every value of type Apple may be converted to Fruit.

Now suppose you have an interface ICompareThings<T> that has a method bool FirstIsBetter(T t1, T t2) that takes two Ts and returns a bool saying whether the first one is better than the second one.

What happens when we provide type arguments? An ICompareThings<Apple> has a method that takes two Apples and compares them. An ICompareThings<Fruit> has a method that takes two Fruits and compares them. But since every Apple is a Fruit, clearly a value of type ICompareThings<Fruit> can be safely used anywhere that an ICompareThings<Apple> is needed. The direction of the convertibility has been reversed—hence the term contravariance.

Perhaps unsurprisingly, the opposite restrictions to those placed on a covariant interface are necessary to ensure safe contravariance. An interface that is contravariant in one of its type parameters must use that type parameter only in input positions such as formal parameters (or in the types of write-only properties, which are extremely rare). You can mark an interface as being contravariant by declaring the type parameter with the in modifier,15 as shown in Listing 12.44.

Listing 12.44: Contravariance Using the in Type Parameter Modifier
class Fruit { }
class Apple : Fruit { }
class Orange : Fruit { }
 
interface ICompareThings<in T>
{
    bool FirstIsBetter(T t1, T t2);
}
 
public class Program
{
 
    private class FruitComparer : ICompareThings<Fruit>
    {
        // ...
    }
    static void Main()
    {
        ICompareThings<Fruit> fc = new FruitComparer();
 
        Apple apple1 = new();
        Apple apple2 = new();
        Orange orange = new();
 
        // A fruit comparer can compare apples and oranges:
        bool b1 = fc.FirstIsBetter(apple1, orange);
        // or apples and apples:
        bool b2 = fc.FirstIsBetter(apple1, apple2);
        // This is legal because the interface is 
        // contravariant.
        ICompareThings<Apple> ac = fc;
        // This is really a fruit comparer, so it can 
        // still compare two apples 
        bool b3 = ac.FirstIsBetter(apple1, apple2);
    }
}

Like covariance support, contravariance uses a type parameter modifier: in, which appears in the interface’s type parameter declaration. This instructs the compiler to check that T never appears on a property getter or as the return type of a method, thereby enabling contravariant conversions for this interface.

Contravariant conversions have all the analogous restrictions as described earlier for covariant conversions: They are valid only for generic interface and delegate types, the varying type arguments must be reference types, and the compiler must be able to verify that the interface is safe for the contravariant conversions.

An interface can be covariant in one type parameter and contravariant in another, although this case seldom arises in practice except with delegates. The Func<A1, A2, ..., R> family of delegates, for example, are covariant in the return type, R, and contravariant in all the argument types.

Lastly, note that the compiler will check the validity of the covariance and contravariance type parameter modifiers throughout the source. Consider the IPairInitializer<in T> interface in Listing 12.45.

Listing 12.45: Compiler Validation of Variance
// ERROR:  Invalid variance: the type parameter 'T' is not
//         invariantly valid
interface IPairInitializer<in T>
{
    void Initialize(IPair<T> pair);
}
// Suppose the code above were legal, and see what goes
//  wrong:
public class FruitPairInitializer : IPairInitializer<Fruit>
{
    // Let’s initialize  our pair of fruits  with an 
    // apple and an orange:
    public void Initialize(IPair<Fruit> pair)
    {
        pair.First = new Orange();
        pair.Second = new Apple();
    }
}
 
// ... later ...
// ...
        var f = new FruitPairInitializer();
        // This would be legal if contravariance were legal:
        IPairInitializer<Apple> a = f;
        // And now we write an orange into a pair of apples:
        a.Initialize(new Pair<Apple>());
        // ...

A casual observer might be tempted to think that since IPair<T> is used only as an input formal parameter, the contravariant in modifier on IPairInitializer is valid. However, the IPair<T> interface cannot safely vary, so it cannot be constructed with a type argument that can vary. As you can see, this would not be type-safe and, in turn, the compiler disallows the IPairInitializer<T> interface from being declared as contravariant in the first place.

Support for Unsafe Covariance in Arrays

So far, we have described covariance and contravariance as being properties of generic types. Of all the nongeneric types, arrays are most like generics; that is, just as we think of a generic “list of T” or a generic “pair of T,” so we can think of an “array of T” as demonstrating the same sort of pattern. Since arrays clearly support both reading and writing, given what you know about covariance and contravariance, you probably would suppose that arrays may be neither safely contravariant nor covariant. That is, you might imagine that an array can be safely covariant only if it is never written to and safely contravariant only if it is never read from—though neither seems like a realistic restriction.

Unfortunately, C# does support array covariance, even though doing so is not type-safe. For example, Fruit[] fruits = new Apple[10]; is perfectly legal in C#. If you then include the expression fruits[0] = new Orange();, the runtime will issue a type-safety violation in the form of an exception. It is deeply disturbing that it is not always legal to assign an Orange into an array of Fruit because it might really be an array of Apples, but that is the situation not just in C# but in all CLR languages that use the runtime’s implementation of arrays.

You should try to avoid using unsafe array covariance. Every array is convertible to the read-only (and therefore safely covariant) interface IEnumerable<T>; that is, IEnumerable<Fruit> fruits = new Apple[10] is both safe and legal because there is no way to insert an Orange into the array if all you have is the read-only interface.

Guidelines
AVOID unsafe array covariance. Instead, CONSIDER converting the array to the read-only interface IEnumerable<T>, which can be safely converted via covariant conversions.

________________________________________

13. This capability was introduced in C# 4.0.
14. Support for safe covariance was added to C# 4.0.
15. Introduced in C# 4.0.
{{ snackbarMessage }}