C# Contravariance: How the in Keyword Works
c# contravariance: Understand C# contravariance through delegates and generic interfaces, including the in keyword, practical usage, and common type-safety boundaries.
c# contravariance requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Contravariance in C#: What the in Keyword Actually Does
In C#, contravariance lets you use a more general type 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 direction — from a more specific type to a more general 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 IComparer<int>:
var comparer = new StringComparer(); IComparer<string> stringComparer = comparer; IComparer<int> intComparer = comparer;
The same comparer instance works for every reference type and every value type that implements the interface. That is a practical benefit: one general-purpose comparer can serve many specific call sites.
Where the in Keyword Can Appear
The in keyword is only legal on a type parameter that 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 in combined with class or struct constraints. A contravariant type parameter cannot have a struct constraint because that would force the parameter to be a value type, which variance does not support.
Runtime Behavior and Type Safety
Contravariance is a compile-time feature. The CLR does not generate different code for a contravariant assignment; it is a reference conversion. Assigning an IComparer<object> to an IComparer<string> variable does not allocate a wrapper or copy the comparer. The same object is referenced, and the compiler has verified that every call through the contravariant interface is safe.
That means there is no measurable runtime cost for using contravariance. The benefit is purely at compile time: the type system catches invalid assignments before the code runs, and the code remains readable because no explicit casts or adapter classes are needed.
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.