Understanding Covariance and Contravariance in C# Generic Interfaces
C# supports variance annotations for generic interfaces through the in and out keywords, enabling more flexible type conversions when working with generics.
The Fundamentals of Variance
At its core, variance refers to how type parameters in generic interfaces can be transformed:
- Covariance: Allows a more derived type to be used where a less derived one is expected.
- Contravariance: Allows a less derived type to be used where a more derived one is expected.
In C#, the standard type conversion follows a specific pattern: a subtype reference can be implicitly converted to a parent type reference (child → parent). Variance extends this concept to generic types.
Type Conversion in Method Signatures
Consider a method that transforms an input parameter:
public string ProcessInput(object input)
{
return input.ToString();
}
This method signature involves two type conversion scenarios:
- Input conversion: When passing arguments to the method, we can use a more specific type (e.g.,
string) where a more general one (e.g.,object) is expected (parent ← child). - Output conversion: When receiving the return value, we can use a more general type (e.g.,
object) where a more specific one (e.g.,string) was produced (child → parent).
Implementing Covariance with out
When a generic interface only uses its type parameter for output, we can apply the out modifier to enable covariance:
interface IReadableCollection<out T>
{
T GetItem(int index);
}
public class StringCollection : IReadableCollection<string>
{
private readonly string[] _items = { "First", "Second", "Third" };
public string GetItem(int index)
{
return _items[index];
}
}
With covariance, we can assign a more specific implementation to a more general interface reference:
IReadableCollection<string> stringCollection = new StringCollection();
IReadableCollection<object> objectCollection = stringCollection;
This works because the type parameter T is only used in output positions, allowing the conversion from IReadableCollection to IReadableCollection.
Implementing Contravariance with in
Conversely, when a generic interface only uses its type parameter for input, we can apply the in modifier to enable contravariance:
interface IProcessor<in T>
{
string Process(T input);
}
public class ObjectProcessor : IProcessor<object>
{
public string Process(object input)
{
return input.ToString();
}
}
With contravariance, we can assign a more general implementation to a more specific interface reference:
IProcessor<object> objectProcessor = new ObjectProcessor();
IProcessor<string> stringProcessor = objectProcessor;
This works because the type parameter T is only used in input positions, allowing the conversion from IProcessor to IProcessor.
Key Considerations
- Variance is only supported for interfaces and delegates - not classes or structs.
- Type parameters marked with
outcan only appear in output positions - as return types or in read-only properties. - Type parameters marked with
incan only appear in input positions - as method parameters or in write-only properties. - Variance annotations must be consistent throughout the interface hierarchy.
Understanding and properly utilizing variance in C# generic interfaces allows for more flexible and reusable code while maintaining type safety.