Back to Blog
C#

C# Generic Covariance: How It Works and Where to Use It

c# generic covariance: Understand C# generic covariance with practical examples: how the out keyword enables safe type substitution in interfaces and delegates.

C#GenericsCovarianceType SafetyIEnumerableDelegates
Diagram showing a covariant generic interface allowing assignment from IEnumerable<string> to IEnumerable<object>

C# generic covariance allows a generic type to accept a more derived type argument when the type parameter is marked with the out keyword. The most familiar case is IEnumerable<string> being assignable to IEnumerable<object> because string derives from object. This behavior is a compile-time feature that preserves type safety without any runtime conversion.

The Core Idea of Covariance in Generic Types

Covariance in generics means that if Derived is a subtype of Base, then SomeGeneric<Derived> is treated as a subtype of SomeGeneric<Base>. In C#, this is only allowed when the type parameter appears in output positions, such as return types, and never in input positions like method parameters. The compiler enforces this restriction to prevent unsafe calls that could break type safety.

Consider a simple interface that exposes a read-only collection. Because you can only get items out of it, not put them in, it is safe to treat a collection of string as a collection of object. The out keyword makes this relationship explicit and verifiable at compile time.

IEnumerable<string> strings = new List<string>(); IEnumerable<object> objects = strings; // Allowed because IEnumerable<out T>

Without covariance, that assignment would fail. The runtime does not need to perform any cast or wrapper; the reference is simply assigned because the type system guarantees that every string is an object.

How the out Keyword Declares Covariance

When you define a generic interface or delegate, you can mark a type parameter with out to declare that it is covariant. This tells the compiler that the type parameter will only be used in output positions. For example:

public interface IProducer<out T> { T Produce(); }

Here, T appears only as a return type. The compiler allows IProducer<Derived> to be assigned to IProducer<Base>. If you try to use T as a parameter, the compiler rejects it:

public interface IProducer<out T> { void Consume(T item); // Compiler error: Invalid variance }

The error occurs because a method that accepts T would allow a caller to pass a Base where a Derived is expected, breaking type safety. The out keyword is a contract that the implementer must honor; the compiler enforces it at the point of declaration.

Covariance in Built-in Interfaces and Delegates

The .NET base class library uses covariance in several widely used types. IEnumerable<out T> is the most common example. Others include IReadOnlyList<out T>, IReadOnlyCollection<out T>, and IGrouping<out TKey, out TElement>. Delegates such as Func<out TResult> are also covariant on their return type.

This means you can write code like:

Func<string> getString = () => "hello"; Func<object> getObject = getString; // Allowed

Similarly, you can pass an IEnumerable<string> to a method that expects IEnumerable<object>:

void PrintAll(IEnumerable<object> items) { /* ... */ } IEnumerable<string> names = new[] { "Ada", "Grace" }; PrintAll(names); // Works because IEnumerable<out T>

This is a practical benefit when you have a method that only reads from a collection. You no longer need to create a new List<object> or use Cast<object>(); the assignment just works.

Defining Your Own Covariant Interface

You can design your own covariant interfaces when the type parameter is only returned, never consumed. A typical scenario is a factory or a read-only repository. For example:

public interface IRepository<out T> { T GetById(int id); IEnumerable<T> GetAll(); } public class InMemoryRepository<T> : IRepository<T> { public T GetById(int id) => default; public IEnumerable<T> GetAll() => Enumerable.Empty<T>(); }

Because T appears only in return positions, you can use IRepository<Derived> where IRepository<Base> is expected. This is useful when you want to expose a read-only view of a repository without forcing callers to know the concrete type.

IRepository<Derived> derivedRepo = new InMemoryRepository<Derived>(); IRepository<Base> baseRepo = derivedRepo; // Allowed

The same rule applies to delegates. If you define a delegate with an out type parameter, it becomes covariant:

public delegate T Factory<out T>(); Factory<string> stringFactory = () => "x"; Factory<object> objectFactory = stringFactory; // Allowed

Keep in mind that covariance is only possible for reference types. Value types like int do not participate in covariance because they do not have inheritance relationships. IEnumerable<int> cannot be assigned to IEnumerable<object> even though int can be boxed to object; the generic variance rules require an implicit reference conversion, not a boxing conversion.

What Covariance Does Not Allow

Covariance does not apply to classes or structs. You cannot declare a covariant class:

public class Wrapper<out T> { } // Compiler error

The out keyword is only valid on interfaces and delegates. This is a deliberate design choice because classes have both input and output members, and enforcing variance on a class would require restricting all methods, which is not practical.

Covariance also does not allow mutation. Even with a covariant interface, you cannot add a method that accepts T. The compiler will reject it. This is why IList<T> is not covariant; it has Add(T) and indexer setters. The read-only interfaces are covariant precisely because they omit those members.

Another limitation is that variance is not transitive across multiple levels in all cases. For example, IEnumerable<KeyValuePair<string, string>> is not automatically assignable to IEnumerable<KeyValuePair<object, object>> because KeyValuePair is a struct and not variant. You need to understand the variance of each generic type in the chain.

Runtime Behavior and Type Safety Considerations

Covariance is a compile-time feature; it has no runtime cost. The assignment is a simple reference copy. There is no casting, no wrapper object, and no performance penalty. The type safety is guaranteed by the compiler, so you do not need runtime checks.

However, covariance can lead to subtle issues if you misuse it in combination with reflection or serialization. For instance, if you serialize an IEnumerable<string> and later deserialize it as IEnumerable<object>, the runtime type is still List<string>. The covariance only affects the compile-time view, not the underlying object. This is usually fine, but be aware that the concrete type remains unchanged.

Another consideration is that covariance can make APIs more flexible but also less explicit. When a method accepts IEnumerable<object>, callers may pass collections of any reference type. This is often desirable, but it can hide the actual element type from the method body. If the method needs to know the specific type, you may need to use generics instead of covariance.

For maintainability, prefer covariant interfaces when you are designing read-only abstractions. This gives consumers more flexibility without sacrificing type safety. But do not force covariance onto an interface that needs input methods; that will only lead to compiler errors and awkward workarounds.

When to Prefer Covariant Design

Use a covariant interface when the type parameter appears only in output positions and you want to enable broader reuse. Common examples are read-only collections, factories, and providers. If you need to both read and write, you cannot use covariance; instead, consider splitting the interface into a read-only covariant part and a separate mutating part.

For instance, you might have IReadOnlyRepository<out T> for queries and IWriteRepository<T> for updates. This allows methods that only read to accept any repository of a derived type, while write methods remain type-safe and non-variant.

When you are consuming a covariant interface, remember that you can only read from it. If you need to add items, you must use a non-covariant interface or a concrete type. This is not a limitation but a safety guarantee.

Finally, be careful with variance in generic method type inference. The compiler uses variance when it infers type arguments, but it may not always infer the most general type. If you run into an inference issue, you can specify the type argument explicitly. This is a minor inconvenience but does not affect the correctness of the covariance model.

c# generic covariance: Practical Usage and Code Examples | RYUSLOG DEV