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 covariance

A 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

  1. Only Interfaces and Delegates: Variance can only be applied to generic interfaces and delegates. Generic classes, structs, and methods do not support the in and out keywords on their type parameters.
  2. Reference Types Only: Generic variance only works with reference types. It does not apply to value types. For example, IEnumerable<int> cannot be assigned to IEnumerable<object> due to the memory layout and boxing overhead associated with value types.
  3. Position Enforcement: The C# compiler strictly enforces position rules. You cannot return a type marked with in from a method, nor can you accept a type marked with out as an input parameter.