C# Generic Contravariance: How It Works
Learn how C# generic contravariance lets base-type implementations work with derived types, including practical interface and delegate examples.
C# generic contravariance lets a generic interface or delegate declared with the in keyword use a less derived type argument than the one in the original declaration. It affects how you assign generic interfaces and delegates, and it is easy to confuse with covariance. Understanding it is essential when designing reusable abstractions that work with base classes and derived types.
What Variance Means in C# Generics
Variance in C# generics describes whether you can substitute one type argument for another in a generic type. Covariance, marked with out, lets you use an IEnumerable<Derived> where an IEnumerable<Base> is expected. Contravariance, marked with in, does the opposite: it lets you use an IComparer<Base> where an IComparer<Derived> is expected.
These rules exist because of the direction of type flow. A covariant type parameter appears only in output positions, such as return values, so a value returned as a more derived type can be safely treated as its base type. A contravariant type parameter appears only in input positions, such as method parameters. That means an implementation written for a base type can accept calls that supply a derived type: an IComparer<Animal> can compare two Dog instances because dogs are animals.
The in Keyword and Contravariant Interfaces
To declare a contravariant generic interface, place the in keyword before the type parameter. The compiler then enforces that the type parameter appears only in input positions and not in output positions such as return values and property getters. The most common example in the .NET base class library is IComparer<in T>.
public interface IComparer<in T> { int Compare(T x, T y); }
Because T appears only as a method parameter, the interface is contravariant. This means you can assign an IComparer<Animal> to a variable of type IComparer<Dog>, even though Dog derives from Animal. The reverse assignment would fail without a cast.
Practical Example: Reusing a Base Comparer
Consider a simple class hierarchy: Animal as the base and Dog as a derived class. A comparer that knows how to compare any Animal can safely compare two Dog instances, because dogs are animals. Here's how that works with IComparer<in T>.
public class Animal { public string Name { get; set; } } public class Dog : Animal { public string Breed { get; set; } } public class AnimalComparer : IComparer<Animal> { public int Compare(Animal x, Animal y) { return string.Compare(x.Name, y.Name); } } IComparer<Dog> dogComparer = new AnimalComparer();
The assignment IComparer<Dog> dogComparer = new AnimalComparer(); compiles because IComparer<in T> is contravariant. The AnimalComparer can handle any Animal, so it can handle Dog. This allows you to write generic code that works with a base comparer and reuse it across multiple derived types without duplication.
Contravariance with Delegates
Delegates also support contravariance when the type parameter appears only in input positions. The Action<in T> delegate is a classic example. You can assign an Action<Animal> to an Action<Dog> because a method that accepts any animal can accept a dog.
public static void DescribeAnimal(Animal animal) { Console.WriteLine($"Name: {animal.Name}"); } Action<Animal> animalAction = DescribeAnimal; Action<Dog> dogAction = animalAction; // Allowed due to contravariance
This is particularly useful in event handling or callback scenarios where you want to register a handler that works on a base type but receive notifications for derived types.
Limitations and Rules
Contravariance is not unrestricted. Several restrictions apply:
- The
inkeyword is allowed only on interfaces and delegate types, not on classes or structs. - Variance conversions apply only when the type argument is a reference type. The type parameter can still be instantiated with a value type, but a value type argument makes the constructed interface or delegate invariant.
- The type parameter can appear only in input positions. If it appears in a return type or a property getter, the compiler raises an error.
- A class that implements a contravariant interface is not automatically contravariant; variance is a property of the interface or delegate declaration, not of the implementation.
These rules are enforced at compile time to prevent unsafe type conversions. The compiler checks every usage of the type parameter and rejects any declaration that violates the direction.
Common Misconceptions and Errors
One frequent mistake is expecting contravariance to work with arrays. Arrays in C# are covariant, but that covariance is unsound and can throw runtime exceptions. Contravariance is not applicable to arrays at all. Another misconception is that a class can declare a contravariant type parameter. That is not allowed; only interfaces and delegates can declare variance.
Another error is trying to use a contravariant type parameter in an output position. For example, if you declare IProducer<in T> and then try to return T from a method, the compiler rejects the declaration. The fix is to either remove the in keyword or redesign the interface so the type parameter only flows inward.
Design Considerations and Maintainability
Contravariance is a tool for API design. When you define a generic interface, ask yourself whether the type parameter is used for input, output, or both. If it is used only for input, marking it in makes the interface more flexible and lets consumers reuse base-type implementations for derived types. This reduces duplication and keeps the API intuitive.
However, contravariance also imposes a constraint: you cannot later add a method that returns the type parameter without breaking the interface. That means you need to be confident about the direction of data flow before you commit to in. If there is any chance the type parameter will be needed in an output position, keep it invariant (no in or out).
Runtime Behavior and Performance
Generic variance in C# is represented in metadata, and for reference type arguments, a contravariant conversion is a reference conversion: no wrapper object is created, no boxing occurs, and the object identity is preserved. When you assign an IComparer<Animal> to an IComparer<Dog>, the runtime still sees the same AnimalComparer; the variance metadata is what makes the assignment valid.
In practical terms, contravariance does not introduce boxing or reflection costs by itself. Its benefit is type safety and code reuse: you can share one base-class implementation across many derived-type usages.
When to Use Contravariance in Your Own Interfaces
If you are designing a generic interface that consumes values of type T, consider marking it in. For example, logging interfaces that accept a message type and repositories that accept a filter type are good candidates. The key is that the type parameter appears only in method arguments, never in return values.
A concrete example is a custom validator interface:
public interface IValidator<in T> { bool Validate(T item); } public class AnimalValidator : IValidator<Animal> { public bool Validate(Animal item) => !string.IsNullOrEmpty(item.Name); } IValidator<Dog> dogValidator = new AnimalValidator();
This lets you register a single validator for a base class and use it for all derived types, which simplifies dependency injection and keeps validation logic centralized. The same pattern applies to handlers, comparers, and any other consumer-style abstraction.
Contravariance is a subtle but powerful feature. It enables code reuse without sacrificing type safety, and it clarifies the direction of data flow in your API. By applying the in keyword deliberately, you can build generic abstractions that are both flexible and maintainable.