Why can't a List<Lion> be a List<Animal>? How my brain finally understood.
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.