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
outkeyword:interface IProducer<out T> - The generic type parameter
Tcan only be used as an output (method return type or read-only property). Tcannot 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
inkeyword:interface IConsumer<in T> - The generic type parameter
Tcan only be used as an input (method argument). Tcannot 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
- Reference Types Only: Variance applies exclusively
to reference types. Value types do not support variance; an
IEnumerable<int>cannot be assigned to anIEnumerable<object>because of boxing requirements. - Interfaces and Delegates Only: Classes and structs
cannot be declared with variant type parameters; only interfaces and
delegates support
inandoutannotations.