Back to Blog
C#

C# Generic In vs Out: Covariance and Contravariance

c# generic in vs out: Understand the difference between `in` and `out` in C# generics, how they enable covariance and contravariance, and when to use them in your own...

C# genericscovariancecontravariancetype safetyinterfaces
Diagram showing covariant and contravariant assignment directions between generic interfaces with base and derived types.

c# generic in vs out requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you declare a generic interface in C#, you can mark its type parameters with in or out. These keywords control how the type parameter can be used inside the interface, and they determine whether the interface is covariant, contravariant, or invariant. The rule is simple: out allows a type parameter to appear only in output positions (return types), while in allows it only in input positions (method parameters). This seemingly small distinction has a large impact on type compatibility and API design.

What Variance Means for Generic Types

Variance describes how a generic type relates to its type arguments when those arguments are substituted with derived or base types. In C#, a covariant interface IProducer<T> can be assigned to IProducer<Base> when you have IProducer<Derived>, because the compiler knows that every Derived is also a Base. Contravariance works in the opposite direction: IConsumer<Base> can be assigned to IConsumer<Derived>, because a consumer that can handle any Base can certainly handle a Derived. Without in or out, a generic interface is invariant, meaning IList<Derived> is not compatible with IList<Base> even if Derived inherits from Base.

The out Keyword: Covariance

The out keyword marks a type parameter as covariant. It can only be used in output positions: as a return type, as a property getter, or as an out parameter. You cannot use it as a method parameter type. The classic example is IEnumerable<T>, which is declared as IEnumerable<out T>. This is why you can assign an IEnumerable<string> to an IEnumerable<object>:

IEnumerable<string> strings = new List<string>(); IEnumerable<object> objects = strings; // Covariance allows this

The compiler knows that IEnumerable<T> only produces T values, never consumes them. So treating a sequence of strings as a sequence of objects is safe. When you define your own covariant interface, you must ensure that the type parameter never appears in a method parameter or a writable property.

The in Keyword: Contravariance

The in keyword marks a type parameter as contravariant. It can only be used in input positions: method parameters, writable properties, or as an in parameter. It cannot be used as a return type. A typical example is IComparer<T>, declared as IComparer<in T>. This allows you to use a comparer for a base type on a derived type:

IComparer<object> objectComparer = Comparer<object>.Default; IComparer<string> stringComparer = objectComparer; // Contravariance allows this

Here, the comparer that knows how to compare any object can also compare strings, because strings are objects. The direction of assignment is reversed compared to covariance. This is safe because the comparer only receives T values; it never returns them.

Practical Example: Building a Covariant Interface

Suppose you are designing an abstraction for a read-only repository. You want to return items of a specific type, but you also want to allow a repository of Customer to be used where a repository of Person is expected. Define the interface with out:

public interface IRepository<out T> { T GetById(int id); IEnumerable<T> GetAll(); } public class CustomerRepository : IRepository<Customer> { public Customer GetById(int id) => new Customer(); public IEnumerable<Customer> GetAll() => new List<Customer>(); } // Usage IRepository<Person> repo = new CustomerRepository(); // Valid because of covariance

The out keyword guarantees that IRepository<T> never accepts a T as input, so the assignment is type-safe. If you later add a method like void Save(T entity), the compiler will reject the out modifier because T would appear in an input position.

Practical Example: Building a Contravariant Interface

For a consumer that processes items, you might want contravariance. Consider an event handler that can handle any Person; it should also be able to handle a Customer. Define the interface with in:

public interface IHandler<in T> { void Handle(T item); } public class PersonHandler : IHandler<Person> { public void Handle(Person item) => Console.WriteLine(item.Name); } // Usage IHandler<Customer> handler = new PersonHandler(); // Valid because of contravariance

The in keyword ensures that T is only used as input, so a handler for a base type can safely handle derived types. This is especially useful for callbacks and event aggregators.

How the Compiler Enforces Variance

The C# compiler enforces variance rules at the declaration site. When you write out T, it checks that T does not appear in any method parameter, property setter, or event accessor. When you write in T, it checks that T does not appear in any return type, property getter, or out parameter. These checks happen at compile time, so you get immediate feedback if you violate the rules. The enforcement is syntactic, not semantic: the compiler does not analyze whether a method actually mutates the object; it only checks the position of the type parameter.

Performance and Runtime Implications

Variance in C# is a compile-time feature. The runtime does not perform any special dispatch or boxing when you use covariant or contravariant assignments. The underlying object is the same; only the static type changes. This means there is no performance penalty for using in or out compared to invariant interfaces. The main cost is in the design constraints: you must carefully structure your interface to fit one of the variance modes. If you need both input and output positions, you cannot use variance and must keep the interface invariant.

Common Mistakes and Misconceptions

A frequent mistake is trying to use out on a type parameter that appears in a method parameter, even if the method does not modify the object. The compiler rejects this because the position is wrong. Another misconception is that variance applies to classes. C# only allows variance on interfaces and delegates, not on classes. You cannot declare class Foo<out T>; the compiler will report an error. Also, variance is not transitive in the way you might expect. If you have IProducer<Derived> and IProducer<Base> where Derived inherits from Base, covariance works. But if you have a generic method that returns IProducer<T>, the variance of T depends on the variance of the interface itself, not on the method's type parameter.

When to Use in and out in Your Own Interfaces

Use out when your interface only produces values of type T, such as a read-only collection, a factory, or a provider. Use in when your interface only consumes values of type T, such as a comparer, a logger, or a command handler. If your interface needs both read and write operations, you cannot use variance; keep it invariant. The decision should be driven by the actual usage patterns. For example, IEnumerable<T> is covariant because it only enumerates. ICollection<T> is invariant because it allows adding and removing. Following these rules makes your APIs more flexible and easier to use correctly.

A final consideration: variance can affect how you implement generic methods. If you have a generic method that takes an IEnumerable<T> and you want to pass a List<string> as IEnumerable<object>, covariance works because IEnumerable<T> is covariant. But if you try to pass a List<string> to a method expecting IList<object>, it fails because IList<T> is invariant. Understanding these constraints helps you choose the right interface for each parameter, avoiding unnecessary casts and preserving type safety.

c# generic in vs out: Practical Usage and Code Examples | RYUSLOG DEV