🎓 A1Learner
🔍
0 of 38 lessons completed

C# Generic Variance

Variance is about when a generic type of one type may stand in for a generic type of another. The answer depends on whether the shape only reads or also writes.

Reading dogs as animals

IEnumerable only reads its members. A list of dogs can be read through it as animals.

using System;
using System.Collections.Generic;

class Animal
{
    public string Name = "";
}

class Dog : Animal
{
}

class Program
{
    static void Main(string[] args)
    {
        List<Dog> dogs = new List<Dog>();
        Dog rex = new Dog();
        rex.Name = "Rex";
        dogs.Add(rex);
        IEnumerable<Animal> animals = dogs;
        foreach (Animal a in animals)
        {
            Console.WriteLine(a.Name);
        }
    }
}

Two dogs through the wide shape

The widened view reads every member in list order. The list itself keeps its Dog type.

using System;
using System.Collections.Generic;

class Animal
{
    public string Name = "";
}

class Dog : Animal
{
}

class Program
{
    static void Main(string[] args)
    {
        List<Dog> dogs = new List<Dog>();
        Dog rex = new Dog();
        rex.Name = "Rex";
        dogs.Add(rex);
        Dog fido = new Dog();
        fido.Name = "Fido";
        dogs.Add(fido);
        IEnumerable<Animal> animals = dogs;
        foreach (Animal a in animals)
        {
            Console.WriteLine(a.Name);
        }
    }
}

A list may not widen

List can both read and add. The compiler refuses to treat a list of dogs as a list of animals.

using System;
using System.Collections.Generic;

class Animal
{
    public string Name = "";
}

class Dog : Animal
{
}

class Program
{
    static void Main(string[] args)
    {
        List<Dog> dogs = new List<Dog>();
        List<Animal> animals = dogs;
        Console.WriteLine(animals.Count);
    }
}

Example

using System;
using System.Collections.Generic;

class Animal
{
    public string Name = "";
}

class Dog : Animal
{
}

class Program
{
    static void PrintNames(IEnumerable<Animal> animals)
    {
        foreach (Animal a in animals)
        {
            Console.WriteLine(a.Name);
        }
    }

    static void Main(string[] args)
    {
        List<Dog> dogs = new List<Dog>();
        Dog rex = new Dog();
        rex.Name = "Rex";
        dogs.Add(rex);
        List<Animal> pets = new List<Animal>();
        Animal plain = new Animal();
        plain.Name = "Plain";
        pets.Add(plain);
        PrintNames(dogs);
        PrintNames(pets);
    }
}

Key points

  • A Dog is an Animal wherever inheritance applies.
  • A read-only shape may widen to the base type.
  • IEnumerable of Animal can read a list of Dog values.
  • A writable shape may not widen.
  • A List of Animal could gain a Cat, so a List of Dog cannot stand in for it.
  • The compiler enforces the rule at compile time.

In this example, PrintNames takes a read-only sequence of animals. The list of dogs passes through the wide shape and prints Rex. The list of animals prints Plain. One reading method serves both lists.

Note

Read-only shapes widen. Writable shapes stay exact.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Why can a List<Dog> be read as IEnumerable<Animal> but not assigned to List<Animal>?