Saturday, June 27, 2026

C# Invariance, Covariance and contravariance Explained

Covariance and Contravariance in C#
Generic interface was introduced in C# version 2.0 and Generic variance - covariance and contravariance - was introduced in C# version 4.0. The out or in modifier with type parameter is used to convert invariant generic type into covariant and contravariant generic respectively. By default a generic type is invariant.

Covariance and contravariance in C# define how type safety and assignment compatibility behave when you pass derived types (subclasses) or base types (superclasses) to generic type parameters, arrays, and delegates. The above concept is abstract and abstruse. Let's understand this concept with the help of an example. 

Consider the following generic interface IContract.
interface IContract<T>
{
    // Note: The position of T determines if it can be covariant or     contravariant
    void Print(T data);
}

Points to Note

  • IContract<T> is an open generic interface because it contains a type parameter T.
  • IContract<string> is a closed generic interface because it contains a type argument - string for type parameter T.
  • Similarly, IContract<int> and IContract<object> are also closed generic interfaces because they contain type arguments - int and object respectively.
Inheritance Relationship between Type Arguments
Note. Throughout the discussion, we assume that:
  • closed generic types belong to the same open generic type.
  • the type arguments are of reference types.
Now, consider the inheritance relationship between type arguments of two closed generic interfaces. For example, IEnumerable<Animal> and IEnumerable<Dog> are closed generic interfaces, and assuming that Dog is a derived class of the Animal class, we can see the inheritance relationship between type arguments of two closed generic interfaces.
  • Every Dog is an Animal.
  • So, every sequence of dogs can be treated as a sequence of animals.
  • So, we can assign a list of dogs to a sequence of animals.
  • That is, IEnumerable<Animal> animals = new List<Dog>(); (This works because IEnumerable<out T> is covariant. Note that a literal List<T> is invariant, so you cannot do List<Animal> list = new List<Dog>();).
We have discussed so far about:
  • open generic interface
  • closed generic interface
  • relationship between type arguments of two closed generic interfaces of the same open generic interface
  • assignment compatibility
Now we discuss invariance, covariance and contravariance.
Invariance, Covariance and Contravariance
The concepts of invariance, covariance and contravariance are involved with closed generics of the same open generic types. Different objects can be created out of closed generics. For example, assuming ContractImpl<T> is a class that implements IContract<T>:
IContract<object> contract1 = new ContractImpl<object>();
IContract<string> contract2 = new ContractImpl<string>();

IEnumerable<Animal> animals = new List<Dog>();

Now the question is, can we use contract1 = contract2; or contract2 = contract1; because both belong to the same open generic IContract<T>?
Whether contract1 = contract2; or contract2 = contract1; is allowed or not depends on defining covariance and contravariance in the open generic IContract<T>.

For example, IContract<T> can be defined as:

  • IContract<out T> for covariance (where T is only used as an output/return type)
  • IContract<in T> for contravariance (where T is only used as an input parameter)
  • The out modifier denotes covariance and the in modifier denotes contravariance.
Interface definition for Covariance
interface IContract<out T>
    void Print(T data);
}
Interface definition for Contravariance
interface IContract<in T>
    void Print(T data);
}
Interface definition for Invariance
interface IContract<T>
    void Print(T data);
}

Note. Looking at the definition of open generic interface, we can know the nature of type parameter i.e. convariance, contravariance or invariance.

The out modifier example (Covariance):
A reference variable which datatype is "closed generic type of supertype" can be assigned an object or reference variable which datatype is "closed generic type of a subtype". For example, a list of dogs assigned to a sequence of animals: 

IEnumerable<Animal> animals = new List<Dog>();

Here the output is a sequence of animals as per the IEnumerable<out T> defined in generic IEnumerable. It means that T can be a return type (output) of an open generic method, enabling a more derived type to be substituted where a less derived type is expected.

Mnemonics: Supertype substituted by Subtype.

Example of Covariance

// definition of open generic interface with out modifier
interface IContract<out T>
{
    T Print();
}
// open generic class implementing open generic interface
class ContractImpl<T> : IContract<T>
{
    public T Print()
    {
        return T
    }
}

class Program
{
    static void Main()
    {
        // closed generics
	IContract<object> objPrinter = new ContractImpl<object>();
    
        // Allowed because of `in`
        IContract<string> strPrinter = objPrinter;

        strPrinter.Print("Hello");
    }
}
The in modifier example (Contravariance):
A reference variable which datatype is "closed generic type of subtype" can be assigned an object or reference variable which datatype is "closed generic type of a supertype". For example, if we have IContract<in T> where T is an input (e.g., void Print(T data);), we can do: 

IContract<Dog> dogContract = new ContractImpl<Animal>();

This is safe because a contract that knows how to handle any general Animal can safely handle a specific Dog when passed as an input.

Example of Contravariance

// definition of open generic interface with out modifier
interface IContract<out T>
{
    T Print();
}
// open generic class implementing open generic interface
class ContractImpl<T> : IContract<T>
{
    private readonly T _value;
    public ContractImpl(T value)
    {
        _value = value;
    }
    public T Print()
    {
        Console.WriteLine($"Type is {typeof(T).FullName}");
        return _value;
    }
}
class Animal
{
    string _name;
    public Animal(string name)
    {
        _name = name;
    }
    public virtual void Info()
    {
        Console.WriteLine($"Animal: Name={_name}");
    }
}
class Dog : Animal
{
    string _name;
    int _age;
    public Dog(string name, int age) : base(name)
    {
        _name = name;
        _age = age;
    }
    public override void Info()
    {
        Console.WriteLine($"Dog: Name={_name}, Age={_age}");
    }
}
class Program
{
    static void Main()
    {
        // 1. Create a Dog contract instance
        IContract<Dog> dogContract = new ContractImpl<Dog>(new Dog("Rocky", 4));

        // ==============================================================
        // 2. DEMONSTRATING COVARIANCE (The power of the 'out' modifier)
        // ==============================================================
        // This assignment is ONLY possible because of 'out T' in IContract<out T>.
        // If you remove 'out' from the interface, this line will crash the compiler!
        IContract<Animal> animalContract = dogContract;

        Console.WriteLine("--- Covariance Demo ---");

        // 3. You can now use the animalContract seamlessly
        Animal animal = animalContract.Print();
        animal.Info(); // Invokes Dog's overridden Info() via polymorphism


        // ===============================================================
        // 4. Practical Real-World Use Case: Passing it to a Method
        // ===============================================================
        Console.WriteLine("\n--- Method Parameter Demo ---");

        // We can pass our dogContract directly into a method expecting an animal contract
        ProcessAnimalContract(dogContract);
    }

    // This method expects an IContract of Animal, but thanks to covariance, 
    // it happily accepts IContract of Dog, Cat, etc.
    static void ProcessAnimalContract(IContract<Animal> contract)
    {
        Animal a = contract.Print();
        Console.WriteLine($"Successfully processed contract for an animal.");
    }
}
Note. We use in or out modifier in open generic type when the type is defined. But we omit in or out modifier in the closed generic types. For example, in IEnumerable<out T> definition:
namespace System.Collections.Generic
{
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
}

But in closed generic type, we omit modifier(in or out):
class Zoo{
    // Method returns IEnumerable<Animal>
    public IEnumerable<Animal> GetAnimals()
    {
        List<Dog> dogs = new List<Dog>
        {
            new Dog { Name = "Tommy" },
            new Dog { Name = "Bruno" }
        };
        // Allowed because IEnumerable<out T> is covariant
        return dogs;
    }
}

Note. The IEnumerator<out T> is also case of covariance.
namespace System.Collections.Generic
{
    public interface IEnumerator<out T> : IEnumerator, IDisposable
    {
        T Current { get; }
    }
}
Invariance
In case of invariance, the type arguments are not considered to be related in any hierarchy. We cannot use what we can do in case of covariance or contravariance.

If a generic type parameter is used as both an input and an output, it must be invariant to preserve type safety at runtime.For example, List<T> is invariant. It allows you to both add items (input) and read items (output). 

Invariance means you must use the exact type specified; it can neither be converted up nor down the inheritance tree. For example,

// This will cause a COMPILE ERROR:
List<Animal> animals = new List<Dog>(); 

Why does the above code throw compile time error? If the code above compiled, you could theoretically execute animals.Add(new Cat());. This would break type safety because a Cat would be injected into what is actually a backing array of Dog objects.

Example of Invariance
The same previous example without in or out modifier makes the case of invariance.
// definition of open generic interface with out modifier
interface IContract<T>
{
    T Print();
}
// open generic class implementing open generic interface
class ContractImpl<T> : IContract<T>
{
    private readonly T _value;
    public ContractImpl(T value)
    {
        _value = value;
    }
    public T Print()
    {
        Console.WriteLine($"Type is {typeof(T).FullName}");
        return _value;
    }
}
class Animal
{
    string _name;
    public Animal(string name)
    {
        _name = name;
    }
    public virtual void Info()
    {
        Console.WriteLine($"Animal: Name={_name}");
    }
}
class Dog : Animal
{
    string _name;
    int _age;
    public Dog(string name, int age) : base(name)
    {
        _name = name;
        _age = age;
    }
    public override void Info()
    {
        Console.WriteLine($"Dog: Name={_name}, Age={_age}");
    }
}
class Program
{
    static void Main()
    {
        IContract<Dog> dogContract = new ContractImpl<Dog>(new Dog("Rocky", 4));
        // 1. Case of subtype to supertype
        // Cannot assign Dog type argument to Animal type argument generic
        // IContract<Animal> dogToAnimal = dogContract; // compile time error

        // 2. Case of supertype to subtype
        IContract<Animal> animalContract = new ContractImpl<Animal>(new Animal("Tommy"));
        // Cannot assign Animal type argument to Dog type argument generic
        // dogContract = animalContract; // compile time error

        // 3. call methods
        Dog dog = dogContract.Print();
        dog.Info();
        Animal animal = animalContract.Print();
        animal.Info();
    }

}
Note that subtype or supertype type substitution is not allowed in case of invariance.

Key Limitations to Keep in Mind
Reference Types Only: Variance rules only apply to reference types. If you use a value type (like int, struct, or bool), the behavior defaults to completely invariant.

Array Covariance Gotcha: Arrays have been covariant since C# 1.0 (e.g., object[] array = new string[10];). However, this implementation is unsafe because it bypasses compile-time checks and will throw an ArrayTypeMismatchException at runtime if you try to insert an invalid type. Always prefer generic collections like IEnumerable<T> to enforce safety at compile time.

No comments:

Post a Comment

Hot Topics