Explain covariance and contravariance in C#.
Quick Answer
Covariance (out keyword) allows using more derived types than specified, used with return types. Contravariance (in keyword) allows using less derived types than specified, used with input parameters. Covariance enables treating derived types as base types for outputs, while contravariance enables treating base types as derived types for inputs.
Detailed Answer
Covariance and Contravariance allow for implicit reference conversion for array types, delegate types, and generic type arguments.
Covariance (out keyword):
- Allows you to use a more derived type than originally specified
- Enables you to assign an instance of a type to a variable of its base type
- Used with return types
- "out" keyword in generic interfaces and delegates
Contravariance (in keyword):
- Allows you to use a less derived type than originally specified
- Used with input parameters
- "in" keyword in generic interfaces and delegates
Covariance Example:
// Class hierarchy
class Animal { }
class Dog : Animal { }
class Cat : Animal { }
// COVARIANCE with IEnumerable
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // OK! Covariance allows this
// You can read Dogs as Animals
// Custom covariant interface
public interface IProducer<out T> // "out" keyword
{
T Produce(); // T only in output position
}
class AnimalProducer : IProducer<Animal>
{
public Animal Produce() => new Animal();
}
class DogProducer : IProducer<Dog>
{
public Dog Produce() => new Dog();
}
// Usage
IProducer<Dog> dogProducer = new DogProducer();
IProducer<Animal> animalProducer = dogProducer; // Covariance!
Animal animal = animalProducer.Produce(); // Returns Dog, treated as Animal
Contravariance Example:
// CONTRAVARIANCE with Action
Action<Animal> animalAction = (animal) => Console.WriteLine("Animal action");
Action<Dog> dogAction = animalAction; // OK! Contravariance allows this
// You can handle Animals where Dogs are expected
// Custom contravariant interface
public interface IConsumer<in T> // "in" keyword
{
void Consume(T item); // T only in input position
}
class AnimalConsumer : IConsumer<Animal>
{
public void Consume(Animal animal)
{
Console.WriteLine("Consuming animal");
}
}
// Usage
IConsumer<Animal> animalConsumer = new AnimalConsumer();
IConsumer<Dog> dogConsumer = animalConsumer; // Contravariance!
dogConsumer.Consume(new Dog()); // Passes Dog to method expecting Animal
Why this works:
Covariance (out): If a method returns a Dog, it's always safe to treat it as an Animal (Dog IS-A Animal)
Contravariance (in): If a method can handle any Animal, it can definitely handle a Dog (Dog IS-A Animal)
Built-in .NET Examples:
// Covariant interfaces
IEnumerable<out T>
IEnumerator<out T>
IQueryable<out T>
IGrouping<out TKey, out TElement>
Func<out TResult>
// Contravariant interfaces
IComparable<in T>
IComparer<in T>
Action<in T>
Predicate<in T>
Real-world Example:
// Covariance - returning more specific types
public interface IAnimalRepository<out T> where T : Animal
{
T GetById(int id);
IEnumerable<T> GetAll();
}
class DogRepository : IAnimalRepository<Dog>
{
public Dog GetById(int id) => new Dog();
public IEnumerable<Dog> GetAll() => new List<Dog>();
}
IAnimalRepository<Dog> dogRepo = new DogRepository();
IAnimalRepository<Animal> animalRepo = dogRepo; // Covariance allows this!
// Contravariance - accepting less specific types
public interface IAnimalValidator<in T> where T : Animal
{
bool Validate(T animal);
}
class AnimalValidator : IAnimalValidator<Animal>
{
public bool Validate(Animal animal)
{
return animal != null;
}
}
IAnimalValidator<Animal> animalValidator = new AnimalValidator();
IAnimalValidator<Dog> dogValidator = animalValidator; // Contravariance!
bool isValid = dogValidator.Validate(new Dog());
Restrictions:
// INVALID - Can't use 'out' parameter in input position
public interface IInvalid<out T>
{
void Process(T item); // ERROR! T is output-only
}
// INVALID - Can't use 'in' parameter in output position
public interface IInvalid<in T>
{
T GetItem(); // ERROR! T is input-only
}
// VALID - Can use invariant (no in/out)
public interface IValid<T>
{
T GetItem();
void Process(T item);
}
Memory Tip:
- COvariance = COmes OUT (return types) → "out"
- CONTRAvariance = goes IN (parameters) → "in"