Covariance and Contravariance in C# .NET

Today in this article, we will learn the concept of Covariance and Contravariance in C# .NET.

Updated October 2026: a section on why List<T> isn’t covariant and fixing invalid variance errors, a section on value types and C# 9 covariant returns, an FAQ, and corrections.

Covariant and contravariant generic type parameters provide flexibility in assigning and using the types. For example, covariant-type parameters enable you to make assignments that look much like ordinary polymorphism. This defined relationship affects the code compilation and ultimately runtime resolution of our types.

Before we dive into the concept of Covariance and Contravariance, Let’s dive into what we already know about the basic OOPS concept. As we know with the OOPS concept C# already has a relationship defined for the object in the form of Inheritance, Polymorphism, and subtyping.

Today in this article, we will cover below aspects,

Natural OOPS principles – a Reminder for Complex type

Let’s try rewinding OOPS – Inheritance and Subtyping.

Let’s take a simple example as shown below,

public class Employee{ }


public class FullTimeEmployee: Employee{ }


public class ContractEmployee: Employee{ }

Let’s understand the above example:

  • Inheritance – FullTimeEmployee and ContractEmployee inherit from the base class Employee.
  • Subtyping – One can easily substitute Employee with FullTimeEmployee or ContractEmployee using subtyping principles. That means you are allowed to do the below,
    • If a method has an input parameter of the type Employee, you can pass an instance of ContractEmployee or FullTimeEmployee as input
    • If a method is declared to return an Employee, you can return a ContractEmployee or a FullTimeEmployee.
    • A subclass overrides a method in a base class, the compiler must check that the overriding method has the right type.

Covariance in Array

Let’s start by looking at what covariance means.

Let’s take an example of an Employee class above discussed using Arrays.

In C# (and . NET), variance is a relation between a type. If two types are given as Base and Derived then,

  • The relationship can be defined between Base and Derived
  • And base class remains higher on the relationship Base ≥ Derived

Let’s take the above example into consideration,

  • Covariance relationship: FullTimeEmployee[] is an Employee[]

Here Employee[] ≥ FullTimeEmployee[]

So it defines the Covariance in Employee if the order of constructed type i.e FullTimeEmployee follows the order Base >Derived

  • Contravariant relationship:Employee[] is FullTimeEmployee[]

Here it defines Contravariance as the order is reversed,

Here Employee[] <= FullTimeEmployee[]

So it defines the ContraCovariance in Employee if the order of constructed type i.e FullTimeEmployee.

  • Invariant relationship : Employee[] is not FullTimeEmployee[] and FullTimeEmployee[] is not an Employee[]

Let’s now see how these defined relationships affect the code compilation and ultimately runtime resolution of our types.

In the above-explained relationship Covariance and Contravariant relationship is not type-safe. This unsafe behavior can be verified using the below code,

           //checking is an array of FullTimeEmployee

            FullTimeEmployee[] checking = { new FullTimeEmployee("001") };

            //accountCollection array of CheckingAccounts

            Employee[] accountCollection = checking;

            //Now accountCollection here is an Array of 
            //FullTimeEmployee but not safe as assigning 
            //ContractEmployee could result in exception 

            accountCollection[0] = new ContractEmployee("001");

In the above example, accountCollection is the collection of FullTimeEmployee. Being able to use FullTimeEmployee or ContractEmployee where Employee is expected shows that Arrays are Covariant.

Later accountCollection is assigned with ContractEmployee which compiles perfectly fine but results in an exception in the runtime. These two types are incompatible the assignment doesn’t work and throws the below error as expected.

“Attempted to access an element as a type incompatible with the array”

Covariance and Contravariance in C# .NET

The above behavior can also be explained using the below simple example

            //fullTime is an array of FullTimeEmployee

            FullTimeEmployee[] fullTime = { new FullTimeEmployee("001") };

            //accountCollection array of FullTimeEmployees

            Employee[] accountCollection = fullTime;

            //Now accountCollection here is an Array of 
            //FullTimeEmployee but not safe as assigning 
            //ContractEmployee could result in exception 

            accountCollection[0] = new ContractEmployee("001");

In the above example, we get the same exception of ArrayTypeMismatchException. there is always a type check to verify the runtime type of the definition of the elements of the array is greater or equal to the instance being assigned to the element.

In the above example, because the runtime type of the array is an array of strings, the first assignment of array elements is valid because string ≥ string and the second is invalid because string ≤ object.

Covariance and Contravariance  in Generics

Covariant and contravariant generics provide greater flexibility in assigning and using generic types. The Base type and Derived type relationship defined above applies to generics only where the type parameter is declared variant on an interface or delegate. By default generic types are invariant, and classes such as List<T> can’t be variant.

The type parameter of the IEnumerable<T> interface is covariant. You can assign an instance of IEnumerable<Derived> to a variable of type IEnumerable<Base>.

Covariance in generic type implemented is using the out generic modifier:

A generic type definition Generic<T> is:

  • Covariant in T if the relationship is defined as Generic<Base> ≥ Generic<Derived>.
  • Contravariant in T if the ordering of the constructed types are reversed from the ordering of the generic type parameters: Generic<Base> ≤ Generic<Derived>.
  • Invariant in T if neither of the above applies.

Below are a few rules for Covariance,

  • A covariant type parameter can be used as the return type of a delegate, and contravariant type parameters can be used as parameter types.
  • For an interface, covariant-type parameters can be used as the return types of the interface’s methods, and contravariant-type parameters can be used as the parameter types of the interface’s methods.
  • A generic interface or generic delegate type can have both covariant and contravariant type parameters.

Starting with the .NET Framework 4, several other generic interfaces have covariant type parameters;

All the type parameters of these interfaces are covariant, so the type parameters are used only for the return types of the members. 

public interface IEnumerable&lt;out T>
{
    IEnumerator&lt;T> GetEnumerator();
}

public interface IEnumerator&lt;out T>
{
    T Current { get; }
    bool MoveNext();
}

Please note the IEnumerable interface is marked as covariant using the out annotation.

Contravariance is enforced in relation to a particular generic type using the in-generic modifier,

.NET Framework 4+ onwards, several other generic interfaces have contravariant type parameters; for example: 

public interface IComparer&lt;in T>
{
    int Compare(T x, T y);
}

These interfaces have only contravariant type parameters, so in the members of the interfaces, the type parameters are used only as parameter types.

Covariance and Contravariance  in Generics Delegates

.NET Framework 4+ onwards, the Func generic delegates are defined as below,

  • Func<T, TResult>– have covariant return types and contravariant parameter types.
  • Action<T1, T2>– The Action generic delegates, have contravariant parameter types.

The above indicates that delegates can be assigned to variables that have more derived parameter types and (in the case of the Func generic delegates) less derived return types.

class Employee { }

    class FullTimeEmployee : Employee { }

    class Program
    {
        
        public delegate Employee HandlerMethod();

        public static Employee AccountHandler()
        {
            return null;
        }

        public static FullTimeEmployee CheckinghHandler()
        {
            return null;
        }

        static void CovarianceTest()
        {
            HandlerMethod handlerMammals = AccountHandler;

            // Covariance is valid here for below assignment 
            HandlerMethod handlerDogs = CheckinghHandler;
        }
    }

Example Contravariance:

In the below example, we shall see with contravariance, you can use one event handler instead of separate handlers and demonstrate delegate’s use with methods that have parameters (whose types are base type of delegate signature parameters).

public delegate void KeyEventHandler(object sender, KeyEventArgs e)

public delegate void MouseEventHandler(object sender, MouseEventArgs e)

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // Use a method that has an EventArgs (Although the event expects the KeyEventArgs).  
            this.button1.KeyDown += this.MultiHandler;

            // Use the same method expects the MouseEventArgs parameter.  
            this.button1.MouseClick += this.MultiHandler;
        }

        // Event handler that accepts a parameter of the EventArgs type.  
        private void MultiHandler(object sender, System.EventArgs e)
        {
            label1.Text = System.DateTime.Now.ToString();
        }
    }

Do you have any comments or ideas or any better suggestions to share?

Please sound off your comments below.

Happy Coding !!



Please bookmark this page and share it with your friends. Please Subscribe to the blog to receive notifications on freshly published (2026) best practices and guidelines for software design and development.



Where variance does not apply: value types and covariant return types

Variance only works when the type argument is a reference type. IEnumerable<string> converts to IEnumerable<object>, but IEnumerable<int> does not.

Why don’t value types work with variance?

Variance relies on implicit reference conversions, where the same object reference is simply viewed as a base type. An int stored in a sequence is raw data, not a reference. Turning it into an object requires boxing, which creates a new heap object for each element. That is a change of representation, not a reinterpretation, so the compiler can’t allow it as a plain conversion.

Note: Think of a reference as a luggage tag pointing to a suitcase. You can relabel the tag (“this is luggage”) without touching the suitcase. A value type is the clothes themselves, loose on the table. To treat them as luggage you must pack each item into a new suitcase first, and variance never does that packing for you.

If you supply a value type as the argument for a variant type parameter, that constructed type is invariant. Variance also doesn’t apply to delegate combination, so you can’t rely on it when you chain delegates together with +.

How are covariant return types different?

Covariant return types, added in C# 9, are a separate feature from generic variance. They apply to inheritance, not to in and out type parameters. An overriding method can declare a more derived return type than the method it overrides, and an overriding read-only property can do the same.

Picture a base class with a factory method that returns Animal. A derived DogFactory class overrides that method and declares its return type as Dog. Callers who hold a DogFactory get a Dog back directly, with no cast. Callers who hold the base factory still see Animal.

C# 14 on .NET 10 doesn’t change any of these rules. Value types stay invariant, and covariant return types remain an override feature.

Frequently asked questions

What is the difference between covariance and contravariance in C#?

Covariance keeps the direction of assignment compatibility, and contravariance reverses it. With a covariant type parameter, marked out, you can assign IEnumerable<Derived> to IEnumerable<Base>. With a contravariant one, marked in, you can assign Action<Base> to Action<Derived>. Covariant type parameters can only appear in output positions such as return types, and contravariant ones only in input positions such as method parameters.

Why can I assign IEnumerable<string> to IEnumerable<object> but not List<string> to List<object>?

Because IEnumerable<out T> only hands out values of type T, so the conversion can’t break type safety. List<T> is a class, and classes can’t declare variant type parameters. It also has methods that accept T, such as Add. If the conversion were allowed, you could add an object that isn’t a string to what is really a list of strings.

Why doesn’t variance work with value types such as int?

Variance relies on implicit reference conversions, and int to object isn’t one. That conversion needs boxing, which changes the representation of the value, so IEnumerable<int> isn’t convertible to IEnumerable<object>. Any constructed type that uses a value type as the argument for a variant type parameter is invariant. If you need the conversion, project the values yourself, for example with Select(i => (object)i).

Are Func and Action covariant or contravariant?

Both, depending on the type parameter. Func<in T, out TResult> is contravariant in its parameter types and covariant in its return type. Action<in T> is contravariant in all its parameters, since it has no return value. This delegate variance has been available since C# 4 and .NET Framework 4, so it works in every current version of .NET.

Leave a Reply

Your email address will not be published. Required fields are marked *