When I started programming in C#, I didn’t understand why it wouldn’t let me assign a List of a derived type to a List of a more general type.

For example, assigning a List<Animal> from a List<Lion>.

There’s a fatal flaw. The setup I needed to be shown in the abstract sense is (but took awhile to piece together):

  • The container types of interest (like a List) have reference semantics.
  • You have two variables pointing to same reference with different types.
  • You do a write or mutation to the wider, more generic type.

More concretely,

List<Lion> lions = new List<Lion>();
List<Animal> animals = lions;  // All lions are animals right?

// .... do lots of things

// But lions and animals point to the *same* backing list reference
animals.append(new Dog()); // You've now appended to 2 lists
                           // and you just broke `lions` wherever that is used.

The key differentiator for me was the difference with reference semantics and value semantics.

My programming life started with Matlab, where matrices have value semantics.

Example:

A = [1 2 3]
B = A

From a end-user perspective, B now is starting from a copy of A. Adding an item to B does not add an item to A.

In the value semantics, independent list case, a List<Animal> being set to a List<Lion> is OK.

From my experience, I think everyone intuitively understands the opposite case: why a List<Animal> cannot be used where a List<Lion> is expected.

List<Animal> animals = new List<Animal> { new Dog() };

List<Lion> = animals; // Already seems wrong
Lion lion = animals[0]; // get dogged

And thus, a List, or any mutable thing that has in-place writes and reads, really should be Invariant, meaning you can’t just substitute one for another, in either direction.