Skip to main content

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:

csharp
// 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:

csharp
// 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:

csharp
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:

csharp
// 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:

csharp
// 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:

  1. Only interfaces and delegates can be covariant in C#
  2. The type parameter must be declared with the out keyword
  3. The type parameter can only appear in output positions (return types)

Let's see an example of invalid covariance usage:

csharp
// 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:

csharp
// 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 out keyword 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> and IReadOnlyList<T>

Covariance works together with its counterpart, contravariance, to provide a more complete type system for C# generics.

Exercises

  1. Create a covariant interface called IMessageProvider<out T> with a method T GetMessage().
  2. Implement this interface for classes that produce different message types (e.g., TextMessage, ImageMessage).
  3. Create a MessageProcessor class that works with IMessageProvider<Message> and demonstrate how covariance allows you to pass providers of more specific message types.

Further Reading



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)