Covariant vs Contravariant Interfaces in C

In C#, covariance and contravariance enable implicit reference conversion for generic type arguments within interfaces and delegates. This article breaks down the fundamental differences between covariance and contravariance, explains the out and in keywords, demonstrates how they regulate data flow, and provides practical code examples to illustrate when and how to apply each variant.

What is Variance?

By default, generic interfaces in C# are invariant. Invariance means that an interface parameterized with a derived type cannot be assigned to the same interface parameterized with its base type, or vice versa. Even though string inherits from object, IList<string> cannot be assigned to IList<object>.

Variance allows you to loosen this restriction safely by marking generic type parameters with the out (covariant) or in (contravariant) modifier.


Covariance (out Keyword)

Covariance preserves assignment compatibility. It allows an interface typed with a more derived class to be assigned to an interface variable typed with a less derived (base) class.

Key Rules

  • Declared using the out keyword: interface IProducer<out T>
  • The generic type parameter T can only be used as an output (method return type or read-only property).
  • T cannot be used as an input parameter for methods.

Why It Exists

If an interface only produces values of type T, returning a more specific type (like string) is completely safe where a more general type (like object) is expected.

Code Example

// Covariant interface
public interface IRepository<out T>
{
    T GetById(int id);
}

public class StringRepository : IRepository<string>
{
    public string GetById(int id) => "Sample Data";
}

// Usage
IRepository<string> strRepo = new StringRepository();
IRepository<object> objRepo = strRepo; // Valid due to covariance

object result = objRepo.GetById(1);

Common .NET examples: IEnumerable<out T>, IReadOnlyList<out T>.


Contravariance (in Keyword)

Contravariance reverses assignment compatibility. It allows an interface typed with a less derived (base) class to be assigned to an interface variable typed with a more derived class.

Key Rules

  • Declared using the in keyword: interface IConsumer<in T>
  • The generic type parameter T can only be used as an input (method argument).
  • T cannot be used as a return type.

Why It Exists

If an interface only consumes values of type T, a handler capable of accepting any general type (like object) can safely handle any specific type (like string).

Code Example

// Contravariant interface
public interface IReceiver<in T>
{
    void Process(T item);
}

public class ObjectReceiver : IReceiver<object>
{
    public void Process(object item) => Console.WriteLine(item.ToString());
}

// Usage
IReceiver<object> objReceiver = new ObjectReceiver();
IReceiver<string> strReceiver = objReceiver; // Valid due to contravariance

strReceiver.Process("Hello, World!");

Common .NET examples: IComparer<in T>, IComparable<in T>, Action<in T>.


Summary of Differences

Feature Covariance Contravariance
Keyword out in
Data Direction Output only (return values, getters) Input only (method arguments)
Assignment Direction More derived \(\rightarrow\) Less derived (Derived to Base) Less derived \(\rightarrow\) More derived (Base to Derived)
Conceptual Role Producer / Supplier Consumer / Handler
Standard Examples IEnumerable<out T>, Func<out TResult> IComparer<in T>, Action<in T>

Restrictions

  1. Reference Types Only: Variance applies exclusively to reference types. Value types do not support variance; an IEnumerable<int> cannot be assigned to an IEnumerable<object> because of boxing requirements.
  2. Interfaces and Delegates Only: Classes and structs cannot be declared with variant type parameters; only interfaces and delegates support in and out annotations.