Covariance and Contravariance

Why IEnumerable<string> is an IEnumerable<object> but List<string> is not a List<object>, and what out and in mean.

Syntaxinterface ISource<out T> { T Get(); } / interface ISink<in T> { void Put(T item); }

Variance decides which generic conversions the compiler allows. A string is an object, so you might expect a List<string> to be a List<object>. It is not, and the reason is sound: if it were, you could hand a list of strings to code that expects a list of objects, and that code could add an int to it. The list would then hold something that is not a string.

IEnumerable<string>, on the other hand, really is an IEnumerable<object>. IEnumerable<T> only ever hands items out. There is no Add on it, so there is no hole to exploit.

out and in

The author of the interface declares this with a keyword on the type parameter. out means covariant: T may appear only in output positions, as a return type or a get-only property. Then IFoo<Derived> converts to IFoo<Base>. The real declaration in the library is interface IEnumerable<out T>.

in means contravariant: T may appear only in input positions, as a method parameter. The conversion then runs the other way. IComparer<in T> means an IComparer<object> can be used wherever an IComparer<string> is wanted, because anything that can order two objects can order two strings.

A type parameter used both ways cannot be marked at all. It is invariant, which is the default, and that is why List<T> and IList<T> convert to nothing.

What trips people up

Only interfaces and delegate types can be variant. A class never is, even one that implements a variant interface. Variance is also not inherited: an interface that extends IEnumerable<out T> must write out again or its own parameter is invariant.

Example

Example · csharp
// IEnumerable<out T> is covariant. T is only ever handed out of it, never
// passed in, so a sequence of strings is also a sequence of objects.
List<string> words = ["pear", "apple"];
IEnumerable<object> things = words;
Console.WriteLine(string.Join(", ", things));

// List<T> is invariant. Items go in as well as out, so this line does not
// compile:   List<object> loose = words;
// If it did, loose.Add(42) would put an int into a list of strings.

// IComparer<in T> is contravariant. T only ever goes in, so a comparer that
// can order any two objects can order any two strings.
IComparer<object> anyOrder = new TextComparer();
IComparer<string> stringOrder = anyOrder;
words.Sort(stringOrder);
Console.WriteLine(string.Join(", ", words));

class TextComparer : IComparer<object>
{
    public int Compare(object? x, object? y)
    {
        // CompareOrdinal compares character codes, so the result never
        // depends on the machine's language settings.
        return string.CompareOrdinal(x?.ToString(), y?.ToString());
    }
}

// Output:
// pear, apple
// apple, pear

When to use it

  • Pass a List<Invoice> straight into a reporting method that takes IEnumerable<object>.
  • Reuse one case-insensitive IComparer<object> to sort several differently typed lists.
  • Expose a read-only view interface that callers can widen to a base type.
  • Explain an ArrayTypeMismatchException in code that assigns through an object[].

More examples

Declaring your own covariant interface

This is the pattern behind IEnumerable<out T> and IReadOnlyList<out T>: split the read-only view of a type into its own interface so callers can widen it. Reach for it when a method only needs to read from a collection of some derived type and you do not want every caller to copy or cast. Note the last comment: implementing a variant interface does not make the class variant.

Example · csharp
// out marks T as covariant, so IReadOnlyBox<string> converts to
// IReadOnlyBox<object>. Adding void Put(T item) to this interface would
// make the compiler reject the out keyword, because writing is what
// would break the conversion.
IReadOnlyBox<string> nameBox = new Box<string>("Ada");
IReadOnlyBox<object> anyBox = nameBox;
Console.WriteLine(anyBox.Get());

// The class itself is invariant, even though the interface is not:
// Box<object> wrong = nameBox;   // does not compile

interface IReadOnlyBox<out T>
{
    T Get();
}

class Box<T> : IReadOnlyBox<T>
{
    private readonly T _value;

    public Box(T value)
    {
        _value = value;
    }

    public T Get() => _value;
}

// Output:
// Ada

Array covariance, and the hole it leaves

Array covariance is the mistake that generic variance was designed to avoid. Because an array can be written to as well as read from, allowing string[] to become object[] moves the type check from compile time to run time. Knowing this explains an otherwise baffling ArrayTypeMismatchException, and explains why IEnumerable<T> got out while List<T> and arrays did not.

Example · csharp
// Arrays of reference types are covariant, and have been since before
// generics existed. The conversion is allowed but it is not safe.
string[] words = ["pear", "apple"];
object[] asObjects = words;    // the language permits this

// The next line compiles, then throws ArrayTypeMismatchException when it
// runs, because the array really is a string[]:
// asObjects[0] = 42;

Console.WriteLine(asObjects[0]);
Console.WriteLine(asObjects.Length);

// Output:
// pear
// 2

Value types do not take part

This is the most common surprise with variance. IEnumerable<T> is covariant, an int can be held as an object, and yet the conversion is refused. Variance is implemented as a reinterpretation of the same reference, so it works only where nothing about the stored value changes. When you really need the object sequence you have to build it, and that boxes every element.

Example · csharp
IEnumerable<int> numbers = [1, 2, 3];

// This does not compile. Variance needs a reference conversion, and int to
// object is boxing, which is a different kind of conversion:
// IEnumerable<object> boxed = numbers;

// Doing it by hand works, because each int is boxed as it is copied.
List<object> boxed = new List<object>();
foreach (int number in numbers)
{
    boxed.Add(number);
}

Console.WriteLine(string.Join(", ", boxed));
Console.WriteLine(boxed.Count);

// Output:
// 1, 2, 3
// 3

Discussion

  • Be the first to comment on this lesson.