C# Covariance
Introduction
When working with C# generics, you'll eventually encounter situations where you want more flexibility when using generic types. Covariance is a powerful feature that allows for this flexibility, particularly when dealing with inheritance relationships. Simply put, covariance allows you to use a more derived type (subclass) where a less derived type (base class) is expected.
Introduced in C# 4.0, covariance (along with its counterpart, contravariance) brings a richer type system to generics and delegates, making your code more reusable and elegant.
What is Covariance?
Covariance allows a method to return a more derived type than what was originally specified. In the context of generics, it means you can assign an instance of IEnumerable<Derived> to a variable of type IEnumerable<Base> when Derived is a subclass of Base.
To indicate that a generic type parameter supports covariance, we use the out keyword in the interface or delegate declaration.
Basic Covariance Example
Let's look at a simple example to understand covariance:
// Base class and derived class
public class Animal { }
public class Dog : Animal { }
// Using covariance with arrays (built-in covariance)
public void CovariantArrayExample()
{
    Dog[] dogs = new Dog[3];
    Animal[] animals = dogs;  // This is allowed because arrays are covariant
    
    // But this can lead to runtime errors
    // animals[0] = new Animal();  // Runtime error: Cannot store Animal in Dog[]
}
The above example demonstrates built-in array covariance, which exists since the early days of .NET. However, array covariance can be unsafe as shown in the commented line which would cause a runtime error.
Covariance with Generic Interfaces
Let's explore covariance with generic interfaces, which is safer because it's enforced at compile-time:
// Using the covariant interface IEnumerable<out T>
public void CovariantInterfaceExample()
{
    IEnumerable<Dog> dogs = new List<Dog>();
    IEnumerable<Animal> animals = dogs;  // This works because IEnumerable<T> is covariant
    
    // We can iterate through our collection of animals
    foreach (Animal animal in animals)
    {
        Console.WriteLine(animal.GetType().Name);
    }
}
Output:
Dog
Dog
Dog
The above example works because IEnumerable<T> is declared with the out keyword:
public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}
The out keyword tells the compiler that T is only ever returned from methods of the interface and never used as an input parameter.
Creating Your Own Covariant Interface
Let's create our own covariant interface:
// Define a covariant interface
public interface IProducer<out T>
{
    T Produce();
}
// Implementation for dogs
public class DogProducer : IProducer<Dog>
{
    public Dog Produce()
    {
        return new Dog();
    }
}
// Using our covariant interface
public void CustomCovariantInterfaceExample()
{
    IProducer<Dog> dogProducer = new DogProducer();
    IProducer<Animal> animalProducer = dogProducer;  // This works because IProducer<T> is covariant
    
    Animal animal = animalProducer.Produce();
    Console.WriteLine(animal.GetType().Name);  // Outputs: Dog
}
Output:
Dog
Covariance with Delegates
Delegates in C# also support covariance. Here's an example:
// Covariance with delegates
public void DelegateCovariance()
{
    Func<object> returnObject = () => new object();
    Func<string> returnString = () => "Hello";
    
    // Covariance allows assigning a delegate that returns a more derived type
    // to a delegate variable expecting a less derived return type
    Func<object> returnObjectFromString = returnString;
    
    object result = returnObjectFromString();
    Console.WriteLine(result);  // Outputs: Hello
}
Output:
Hello
Covariance Restrictions
Covariance has some important restrictions:
- Only interfaces and delegates can be covariant in C#
- The type parameter must be declared with the outkeyword
- The type parameter can only appear in output positions (return types)
Let's see an example of invalid covariance usage:
// This won't compile
public interface IBadProducer<out T>
{
    T Produce();       // Valid - T is in output position
    void Consume(T t); // Invalid - T is in input position
}
Real-World Application: Factory Pattern
Covariance is particularly useful in design patterns like the Factory pattern:
// Product hierarchy
public abstract class Product { }
public class ConcreteProductA : Product { }
public class ConcreteProductB : Product { }
// Covariant factory interface
public interface IFactory<out TProduct> where TProduct : Product
{
    TProduct Create();
}
// Concrete factories
public class FactoryA : IFactory<ConcreteProductA>
{
    public ConcreteProductA Create()
    {
        return new ConcreteProductA();
    }
}
public class FactoryB : IFactory<ConcreteProductB>
{
    public ConcreteProductB Create()
    {
        return new ConcreteProductB();
    }
}
// Client code
public void UseFactories()
{
    IFactory<ConcreteProductA> factoryA = new FactoryA();
    IFactory<ConcreteProductB> factoryB = new FactoryB();
    
    // Because of covariance, we can store these in a list of IFactory<Product>
    List<IFactory<Product>> factories = new List<IFactory<Product>>
    {
        factoryA,  // Works because IFactory is covariant
        factoryB   // Works because IFactory is covariant
    };
    
    // We can now create products without caring about their specific type
    foreach (var factory in factories)
    {
        Product product = factory.Create();
        Console.WriteLine($"Created a {product.GetType().Name}");
    }
}
Output:
Created a ConcreteProductA
Created a ConcreteProductB
Summary
Covariance is a powerful feature in C# that allows for more flexible and reusable generic code. To recap:
- Covariance allows you to use a more derived type (subclass) where a less derived type (base class) is expected
- It's indicated by the outkeyword on generic type parameters
- It only works with interfaces and delegates in C#
- Covariant type parameters can only appear in output positions (return types)
- Common covariant interfaces in .NET include IEnumerable<T>andIReadOnlyList<T>
Covariance works together with its counterpart, contravariance, to provide a more complete type system for C# generics.
Exercises
- Create a covariant interface called IMessageProvider<out T>with a methodT GetMessage().
- Implement this interface for classes that produce different message types (e.g., TextMessage,ImageMessage).
- Create a MessageProcessorclass that works withIMessageProvider<Message>and demonstrate how covariance allows you to pass providers of more specific message types.
Further Reading
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!