C# Contravariance: How the in Keyword Works
Learn how C# contravariance works with delegates and generic interfaces, including the in keyword, practical usage, and common type-safety boundaries.
Contravariance in C# lets an interface or delegate that works with a more general type be used where a more specific type is expected. The most visible form is the in keyword on a generic type parameter:
public interface IComparer<in T> { int Compare(T x, T y); }
Because T is marked in, an IComparer<object> can be assigned to a variable of type IComparer<string>. The comparer that knows how to compare any object is also capable of comparing strings. That assignment direction — from a more general interface type to a more specific one — is what distinguishes contravariance from covariance.
Contravariance in Delegates
The most common place developers encounter contravariance is the Action<T> delegate family. Action<object> can be assigned to Action<string>:
Action<object> log = obj => Console.WriteLine(obj); Action<string> logString = log;
The method assigned to log accepts any object. Passing it a string is always safe because a string is an object. The compiler allows this assignment because the delegate's parameter type is contravariant.
The same rule applies to custom delegates. A delegate declared with in on its parameter type is contravariant:
public delegate void Handler<in T>(T value);
Handler<object> can be assigned to Handler<string> for the same reason: a handler that accepts any object can accept a string.
Contravariance in Generic Interfaces
The BCL exposes several contravariant interfaces. IComparer<in T>, IEqualityComparer<in T>, and IComparable<in T> are the ones you are most likely to use directly.
Consider a comparer that sorts objects by their string representation:
public sealed class StringComparer : IComparer<object> { public int Compare(object x, object y) { return string.Compare(x?.ToString(), y?.ToString()); } }
Because IComparer<T> is contravariant, this comparer can be passed to any API that expects IComparer<string> or a comparer for another reference type:
var comparer = new StringComparer(); IComparer<string> stringComparer = comparer;
The same comparer instance can serve many reference-type call sites, but not value-type call sites. That is a practical benefit: one general-purpose comparer can be reused where a comparer for a specific reference type is expected.
Where the in Keyword Can Appear
Variance annotations are legal only on interface and delegate type parameters. On such a type parameter, in is allowed only when the type parameter appears in input positions. The parameter may appear as a method parameter type, but not as a method return type, and not as the type of a public property.
public interface IConsumer<in T> { void Accept(T value); // allowed // T Produce(); // not allowed // T Value { get; } // not allowed }
The compiler enforces this because contravariance guarantees that an IConsumer<object> can stand in for an IConsumer<string>. If Produce() returned a T, the object-based implementation would return an object where the caller expects a string. That would break type safety at runtime.
The same restriction applies to ref and out parameters. A contravariant type parameter cannot appear as a ref or out parameter because those positions are both input and output.
Practical Usage: Comparers and Equality
The most common real-world use of contravariance is passing a general comparer to a method that requires a specific one.
public static T Max<T>(T first, T second, IComparer<T> comparer) { return comparer.Compare(first, second) >= 0 ? first : second; }
Because IComparer<T> is contravariant, you can pass an IComparer<object> to this method when T is string:
IComparer<object> objectComparer = new StringComparer(); string result = Max("apple", "banana", objectComparer);
Without contravariance, you would need to wrap the comparer or create a new one for each type. Contravariance removes that boilerplate.
Common Mistakes and Boundaries
A frequent mistake is assuming that contravariance works in both directions. It does not. IComparer<string> cannot be assigned to IComparer<object>, because a string-only comparer cannot compare arbitrary objects.
Another boundary is value types. Variance in C# does not apply to value types. IComparer<object> cannot be assigned to IComparer<int> even though int boxes to object. The runtime treats value types as distinct from their boxed form for variance purposes, so the compiler rejects the assignment.
A third boundary is type parameter constraints. A contravariant type parameter cannot have a struct constraint, because that would force the type argument to be a value type and variance does not support value types. A class constraint is compatible with variance.
Runtime Behavior and Type Safety
Contravariance is a compile-time feature. Assigning an IComparer<object> to an IComparer<string> variable is a reference conversion: no wrapper is allocated and the comparer is not copied. The same object is referenced, and the compiler has verified that every call through the contravariant interface is safe.
Because the benefit is at compile time, the type system catches invalid assignments before the code runs, and the code remains readable without explicit casts or adapter classes.
When Contravariance Is Not the Right Tool
Contravariance is useful when you have a general-purpose implementation that should serve many specific call sites. It is not useful when the implementation depends on the concrete type. A comparer that sorts by a property only present on Customer cannot be contravariant to object; it must be an IComparer<Customer>.
Similarly, if you need to produce values of the type parameter, contravariance is impossible by definition. In that case, covariance (out) or invariance is the correct choice. The decision comes down to the direction of data flow: if the type parameter flows into methods, contravariance applies; if it flows out, covariance applies; if it flows both ways, the type must be invariant.