How to Use Covariance with IEnumerable in C
Covariance allows you to use a more derived type than originally
specified in a generic interface. In C#,
IEnumerable<T> is covariant because its generic type
parameter is marked with the out keyword
(IEnumerable<out T>). This guide explains how
covariance works with IEnumerable<T>, how to leverage
it when assigning derived collections to base references, and how to
implement your own custom covariant collection wrappers.
What is Covariance?
Covariance preserves assignment compatibility for generic types. If
class Dog inherits from class Animal,
covariance allows an instance of IEnumerable<Dog> to
be assigned directly to a variable of type
IEnumerable<Animal>.
This is safe because IEnumerable<out T> is a
read-only interface. It only outputs data (T appears only
as a return type). Because you can only read from it, reading a
Dog as an Animal will never violate type
safety.
In contrast, List<T> is invariant because it
allows both input and output operations (like
.Add(T item)). You cannot assign a
List<Dog> to a List<Animal>.
Using Covariance
with Built-in IEnumerable<T>
Because the .NET framework defines
IEnumerable<out T> with the out
parameter, covariance works automatically with reference types.
public class Animal
{
public string Name { get; set; } = string.Empty;
}
public class Dog : Animal
{
public void Bark() => Console.WriteLine("Woof!");
}
public class Program
{
public static void Main()
{
List<Dog> dogs = new List<Dog>
{
new Dog { Name = "Buddy" },
new Dog { Name = "Rex" }
};
// Covariance in action: List<Dog> implements IEnumerable<Dog>,
// which can be assigned to IEnumerable<Animal>.
IEnumerable<Animal> animals = dogs;
PrintAnimalNames(dogs);
}
public static void PrintAnimalNames(IEnumerable<Animal> animals)
{
foreach (var animal in animals)
{
Console.WriteLine(animal.Name);
}
}
}How to Implement a Custom Covariant Interface
To implement covariance in your own generic interfaces, you must
explicitly declare the generic parameter with the out
keyword.
1. Define the Covariant Interface
The out keyword specifies that T is
covariant. The type T can only be used as a method return
type, a property get accessor return type, or in a nested
covariant position.
using System.Collections;
using System.Collections.Generic;
// The 'out' keyword marks T as covariant
public interface IReadOnlyRepository<out T> : IEnumerable<T>
{
T GetById(int id);
IEnumerable<T> GetAll();
}If you attempt to use T as a method argument (e.g.,
void Add(T entity);), the compiler will throw an error
because input positions require contravariance (in T) or
invariance.
2. Implement the Interface
The implementing class does not use the out keyword.
Classes are always invariant in C#.
public class ReadOnlyRepository<T> : IReadOnlyRepository<T>
{
private readonly List<T> _items;
public ReadOnlyRepository(List<T> items)
{
_items = items;
}
public T GetById(int id) => _items[id];
public IEnumerable<T> GetAll() => _items;
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}3. Consume the Covariant Interface
You can now pass an instance parameterized with a derived type to a reference parameterized with a base type:
IReadOnlyRepository<Dog> dogRepo = new ReadOnlyRepository<Dog>(new List<Dog> { new Dog { Name = "Max" } });
// Valid due to covariance
IReadOnlyRepository<Animal> animalRepo = dogRepo;
Animal animal = animalRepo.GetById(0);
Console.WriteLine(animal.Name);Limitations of Covariance
- Reference Types Only: Covariance only applies to
reference types.
IEnumerable<int>cannot be converted toIEnumerable<object>becauseintis a value type, and value types require boxing, which changes the underlying memory layout. - Output Only: You cannot pass
Tas a parameter to any method inside a covariant interface. - Classes Cannot Be Covariant: Only interfaces and
delegates can be declared as covariant. Concrete classes like
List<T>orReadOnlyCollection<T>cannot use theoutkeyword.