C# Generic Contravariance: How It Works
c# generic contravariance: Learn how C# generic contravariance lets you pass less derived types to generic interfaces, with practical examples and limitations.
C# generic contravariance allows a generic type parameter declared with the in keyword to accept a less derived type than the one specified in the original declaration. This is a compile-time feature that 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 a type parameter with a more derived type (covariance) or a less derived type (contravariance). 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 (return values), so a more derived value can be safely treated as a less derived one. A contravariant type parameter appears only in input positions (method arguments), so a less derived value can be passed to a method that expects a more derived one, because the method will only call members available on the base type.
The in Keyword and Contravariant Interfaces
To declare a contravariant generic interface, you place the in keyword before the type parameter. This tells the compiler that the type parameter is used only for input, never for return values. 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 certainly 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 a free-for-all. Several restrictions apply:
- The
inkeyword is allowed only on interfaces and delegate types, not on classes or structs. - The type parameter must be a reference type. Value types, such as
intorstruct, do not support variance. - 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.
- Variance does not apply to generic classes, only to interfaces and delegates.
- The variance is not inherited through inheritance. A class that implements a contravariant interface is not automatically contravariant.
These rules are enforced at compile time to prevent unsafe type conversions. The compiler checks every usage of the type parameter and rejects any that violate 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 you can use a contravariant interface with a class 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 will reject it with an error like Invalid variance: The type parameter 'T' must be invariantly valid on 'IProducer<T>.Produce()'. 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
Contravariance is a compile-time feature. It does not change the runtime type of objects or introduce any performance overhead. The generated IL is identical whether you use a contravariant assignment or a direct cast. The only effect is that the compiler allows certain assignments that would otherwise be rejected.
This means you can use contravariance freely without worrying about boxing, casting, or reflection costs. The benefit is purely in type safety and code organization. When you assign an IComparer<Animal> to an IComparer<Dog>, the runtime still sees the same object; the variance only tells the compiler that the assignment is safe.
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, a logging interface that accepts a message type, or a repository that accepts 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.