Array Covariance in C#

When we first learn about inheritance, we’re given some plain-English examples with an emphasis on the phrase “is-a”. A car is-a vehicle, so a Car class would inherit from a Vehicle class. The idea that class inheritance can so directly map to English is seriously misleading and can get you into trouble.

In English, it’s fair to say that a bag of apples is-a bag of objects. It may have an odd sound to it, but it’s true. This makes it very tempting to say that in C#, a List<Apple> should be assignable to a List<Object>, since all Apple instances are also Object instances:

List<Apple> apples = new List<Apple> { new Apple(), new Apple() };
List<Object> objects = apples;

The sample above won’t compile. The compiler is protecting us from potential runtime type exceptions. If this compiled, we could get into trouble like so:

List<Apple> apples = new List<Apple> { new Apple(), new Apple() };
List<Object> objects = apples;
objects[0] = "This is not an apple.";

If this compiled, we would be able to attempt to put a string into a collection that simply does not support that operation. The English metaphor for inheritence (is-a) just doesn’t adequately match the actual constraints of the type system. We’re trying to violate the Liskov Substitution Principle. If we declared that List<Apple> is a subtype of List<Object>, we’d be saying that every operation that acts on a List<Object> could be given a List<Apple>, and that’s simply not true. In our example, the operations performed on the List<Object> included putting new arbitrary objects into the list, and you just can’t do that to a List<Apple>.

Because List<Apple> has more constraints than List<Object>, List<Apple> is not assignment-compatible with List<Object>.

Unfortunately, the previous example does compile when you replace List<T> with supposedly-strongly-typed arrays. Instead of knowing that you’ve done something wrong at compile time, you get an exception at runtime:

Apple[] apples = new[] { new Apple(), new Apple() };
Object[] objects = apples;
objects[0] = "This is not an apple."; //Throws ArrayTypeMismatchException at runtime!

Because TChild[] can be assigned to TParent[] for any TChild inheriting from TParent, we can say “arrays in C# are covariant”.

To sum up, List<Apple> can’t be treated as List<Object> because of the operations that List<T> provides. The question, then, is could we ever safely take advantage of this convenient funky-casting for other generic types? The answer to this question falls under the category of “Covariance and Contravariance” and is provided by the C# 4 keywords in and out. In my next post, we’ll cover an example where this funky-casting is both convenient and safe.