Covariance and Contravariance in C# .NET
Today in this article, we will learn the concept of Covariance and Contravariance in C# .NET.
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”
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 already holds true for generics also.
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<out T>
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<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<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(2024) best practices and guidelines for software design and development.