An enumerable of dogs can safely be viewed as an enumerable of animals because every value it produces is an animal. A mutable list of dogs cannot safely become a mutable list of animals, because someone could then insert a cat. These two situations explain why some generic conversions are permitted while superficially similar ones are rejected.
This guide uses .NET 8 and C# 12. You will design a producer with out, a consumer with in, and trace the safe conversion direction. The topic builds on generic interfaces rather than repeating generic constraints. A constraint restricts which types can fill a parameter; variance describes particular conversions between already constructed interface or delegate types.
Covariance safely widens produced reference values
A covariant type parameter is declared with out. It can be used in appropriate output positions, such as a method's return type. A source that produces a derived reference type can then be viewed through an interface that produces a base reference type. The caller asks for an animal and receives a dog, which satisfies the promise.
IEnumerable<T> is a familiar covariant interface. An IEnumerable<string> can be assigned to IEnumerable<object> because each produced string is an object. The conversion changes the reference through which the sequence is used; it does not rebuild every element into a new collection. Avoid saying that covariance makes the underlying object mutable in a broader way. The allowed interface still limits the operations the caller can perform.
Contravariance safely narrows accepted reference values
A contravariant type parameter is declared with in. It can be used in appropriate input positions, such as a method argument. A consumer capable of accepting every animal can safely stand in for a consumer asked to accept dogs. Every dog supplied by the narrower caller is something the broader consumer already knows how to accept.
The direction can feel reversed until you phrase it as a capability. A dog-only consumer cannot promise to accept arbitrary animals, so it cannot replace an animal consumer. An animal consumer has the broader capability and can satisfy the narrower dog contract. Action<T> demonstrates this pattern. An Action<object> can be used where an Action<string> is required, because the target action can accept each string.
Mutable collections are invariant for a reason
List<T> is invariant. A List<Dog> cannot be assigned to List<Animal>, even though Dog derives from Animal. A mutable list both returns and accepts values. Allowing that assignment would let a caller insert a cat into a list whose original users expect only dogs.
You can expose a suitable covariant read-only interface when callers only need to observe produced values. That does not freeze the underlying collection: another owner might still modify it. If the API promises a stable snapshot, materialize or otherwise enforce that promise separately. Type-safe access and immutability are different properties. Do not use unsafe casts or broad object containers to defeat a compiler restriction that protects a real collection invariant.
Variance belongs to interfaces and delegates
C# permits these variance annotations on appropriate generic interface and delegate type parameters, not ordinary class type parameters. A class can implement a variant interface, and a caller can use that interface, but the class itself does not become covariant. Only the declared applicable interface or delegate conversion is available.
Variance conversions apply to reference types. An IEnumerable<int> does not become an IEnumerable<object> through generic covariance, even though each integer can be boxed. Boxing every element would be a different operation, such as a projection. This limitation keeps variance as a reference conversion rather than a hidden transformation of the sequence. Recognize the difference when a compiler rejects a conversion involving structs.
Arrays have a separate legacy covariance rule
Reference-type arrays allow conversions such as a string array to an object array. Unlike the safe producer-only interface conversion, arrays remain mutable. Assigning an incompatible object through the widened array reference can therefore throw ArrayTypeMismatchException at runtime. This rule is separate from generic variance and is not a model for designing mutable generic containers.
Prefer interfaces that express the actual capability needed by an API. If a method only reads animals, accepting IEnumerable<Animal> can support many producer implementations. If it must mutate a specific collection, its signature should acknowledge that stronger requirement. Keep the variance annotations minimal and let compiler diagnostics reveal illegal input/output positions rather than obscuring them through casts.
Official references
C# Generic Covariance and Contravariance Examples
Each example shows the code, the output it produces and what each part does.
Producer and consumer conversions
This complete program uses two small variant interfaces. A dog source is widened to an animal source, while a general animal consumer is narrowed to a dog consumer.
No element conversion or copying is required. The interface declarations constrain what implementations and callers can do, making these reference conversions safe.
using System;
IProducer<Animal> producer = new DogProducer();
Console.WriteLine(producer.Get().Name);
IConsumer<Dog> consumer = new AnimalPrinter();
consumer.Accept(new Dog("Rex"));
public class Animal
{
public string Name { get; }
public Animal(string name) => Name = name;
}
public sealed class Dog : Animal
{
public Dog(string name) : base(name) { }
}
public interface IProducer<out T>
{
T Get();
}
public interface IConsumer<in T>
{
void Accept(T value);
}
public sealed class DogProducer : IProducer<Dog>
{
public Dog Get() => new("Milo");
}
public sealed class AnimalPrinter : IConsumer<Animal>
{
public void Accept(Animal value) =>
Console.WriteLine("Accepted: " + value.Name);
}Milo
Accepted: RexC# Generic Covariance and Contravariance in Real Projects
Worked exercise: reuse a broad callback
The task is to reuse a callback that accepts any object for a workflow that supplies only strings. Declare the callback under the broad contract, assign it to the narrower delegate type, and invoke the narrower reference. Then demonstrate a covariant enumerable separately so the two directions are visible.
using System;
using System.Collections.Generic;
Action<object> describe = value =>
Console.WriteLine("Value: " + value);
Action<string> describeName = describe;
describeName("Asha");
IEnumerable<string> names = new[] { "Asha", "Bilal" };
IEnumerable<object> objects = names;
foreach (object value in objects)
Console.WriteLine(value);Value: Asha
Asha
BilalThe action's input capability is broad enough for every string. The sequence's output guarantee is narrow enough to satisfy every object request. Writing these statements in words is a reliable way to check direction instead of memorizing arrows without understanding their safety argument.
C# Generic Covariance and Contravariance FAQ
Is covariance the same as inheritance?
It relies on a compatible reference relationship between type arguments, but it is a rule for constructed generic interfaces or delegates. A covariant producer of dogs can be used as a producer of animals. That does not mean List<Dog> derives from List<Animal>. The declared generic contract and allowed operations determine whether the constructed-type conversion is available.
Why cannot a covariant producer accept T as an ordinary argument?
Accepting a value through a widened producer would allow a caller to supply a base-type value that the original implementation may not support. A dog producer viewed as an animal producer must not suddenly accept cats. The compiler restricts type-parameter positions to preserve the safe conversion. Design a separate consumer contract when accepting values is part of the responsibility.
Why does variance not turn IEnumerable<int> into IEnumerable<object>?
An integer is a value type, and boxing each element into an object would require an element transformation rather than a simple reference conversion. Generic variance conversions are for reference-type arguments. If the transformation is intended, use an explicit projection or other appropriate operation. That makes allocation and representation changes visible instead of hiding them behind an assignment.
Does a read-only interface make the collection immutable?
It limits what a caller can do through that interface. Another reference may still mutate the same collection. A covariant read-only view therefore offers type-safe observation, not an automatic frozen snapshot. If stability matters, return a separately owned snapshot or a collection with a documented immutability contract. Treat access capability and state lifetime as separate design decisions.
C# Generic Covariance and Contravariance Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Why is List<Dog> not assignable to List<Animal>?
C# Generic Covariance and Contravariance Quiz
2 questions to check what you learned.
Why can a producer of Dog be used as a producer of Animal?
- The conversion permits inserting any Animal into the producer.
- Every produced Dog satisfies the promised Animal output.
- All generic classes inherit from each other automatically.
- The runtime converts every Dog into a newly allocated Animal.
Which consumer can safely stand in for a consumer of Dog?
- A producer of Animal.
- A consumer that accepts only one particular Dog instance.
- A consumer able to accept any Animal.
- A mutable List<Animal> assigned to List<Dog>.