C# Generic Variance: The Out and In Keywords
Generic variance in C# controls how type compatibility works between
generic types that share an inheritance relationship. By using the
out and in keywords on generic type parameters
in interfaces and delegates, C# enables covariance and contravariance.
This overview explains how these keywords change type assignment rules,
allowing developers to write more reusable, flexible, and type-safe code
when dealing with polymorphic collections and delegates.
Understanding Invariance
By default, generic type parameters in C# are invariant. Invariance
means that a generic type Gen<Base> and
Gen<Derived> are treated as completely unrelated,
regardless of the relationship between Base and
Derived.
List<string> strings = new List<string>();
// This causes a compile-time error:
// List<object> objects = strings; Even though string inherits from object,
List<string> cannot be assigned to
List<object>. Invariance prevents unsafe operations,
such as adding an int to a list that was originally
instantiated as a list of strings.
Covariance with the
out Keyword
The out keyword enables covariance.
Covariance allows you to use a more derived (more specific) type than
the one originally specified.
When a type parameter is marked with out, it signifies
that the type parameter is used only in output
positions—such as method return types or read-only properties. Because
data only comes out of the object, it is impossible to introduce
incompatible types into the instance.
Example:
IEnumerable<out T>
A classic example in the .NET runtime is
IEnumerable<out T>:
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}Because T is marked with out, the following
assignment is valid:
IEnumerable<string> strings = new List<string> { "apple", "banana" };
IEnumerable<object> objects = strings; // Allowed because of covarianceA consumer of IEnumerable<object> only reads
object instances out of the sequence. Since every
string is an object, this operation is
completely type-safe.
Contravariance with the
in Keyword
The in keyword enables contravariance.
Contravariance allows you to use a less derived (more general/broader)
type than the one originally specified.
When a type parameter is marked with in, it signifies
that the type parameter is used only in input
positions—such as method arguments. Because data is only passed into the
object, the implementation can safely accept any base type that
satisfies the method's parameter requirements.
Example:
Action<in T> and
IComparer<in T>
A built-in example is the Action<in T>
delegate:
public delegate void Action<in T>(T obj);Consider the following assignment:
Action<object> printObject = obj => Console.WriteLine(obj.ToString());
// Allowed because of contravariance:
Action<string> printString = printObject;
printString("Hello World");printObject can handle any object.
Therefore, passing it a string (which is an
object) is safe. Contravariance allows a broader consumer
(Action<object>) to satisfy a more specialized
contract (Action<string>).
Key Rules and Limitations
- Only Interfaces and Delegates: Variance can only be
applied to generic interfaces and delegates. Generic classes, structs,
and methods do not support the
inandoutkeywords on their type parameters. - Reference Types Only: Generic variance only works
with reference types. It does not apply to value types. For example,
IEnumerable<int>cannot be assigned toIEnumerable<object>due to the memory layout and boxing overhead associated with value types. - Position Enforcement: The C# compiler strictly
enforces position rules. You cannot return a type marked with
infrom a method, nor can you accept a type marked withoutas an input parameter.