Back in 2009 Eric teased us that things would change in C# 4. So where do we stand today?
The classes used in my answer can be found at the bottom. To make this easier to follow, we will use a Mammal
class as "parent", and Cat
and Dog
classes as "children". Cats and dogs are both mammals, but a cat is not a dog and a dog is not a cat.
This still isn't legal, and can't be:
List<Cat> cats = new List<Cat>();
List<Mammal> mammals = cats;
Why not? Cats are mammals, so why can't we assign a list of cats to a List<Mammal>
?
Because, if we were allowed to store a reference to a List<Cat>
in a List<Mammal>
variable we would then be able to compile the following code to add a dog to a list of cats:
mammals.Add(new Dog());
We mustn't allow that! Remember, mammals
is just a reference to cats
. Dog
does not descend from Cat
and has no business being in a list of Cat
objects.
Starting with .NET Framework 4, several generic interfaces have covariant type parameters declared with the out
Generic Modifier keyword introduced in C# 4. Amongst these interfaces is IEnumerable<T>
which of course is implemented by List<T>
.
That means we can now cast a List<Cat>
to an IEnumerable<Mammal>
:
IEnumerable<Mammal> mammalsEnumerable = cats;
We can't add a new Dog
to mammalsEnumerable
because IEnumerable<out T>
is a "read-only" interface i.e. it has no Add()
method, but we can now use cats
wherever a IEnumerable<Mammal>
can be consumed. For example, we can concatenate mammalsEnumerable
with a List<Dog>
to return a new sequence:
void Main()
{
List<Cat> cats = new List<Cat> { new Cat() };
IEnumerable<Mammal> mammalsEnumerable =
AddDogs(cats); // AddDogs() takes an IEnumerable<Mammal>
Console.WriteLine(mammalsEnumerable.Count()); // Output: 3. One cat, two dogs.
}
public IEnumerable<Mammal> AddDogs(IEnumerable<Mammal> parentSequence)
{
List<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
return parentSequence.Concat(dogs);
}
Class definitions:
public abstract class Mammal { }
public class Cat: Mammal { }
public class Dog : Mammal { }