C# supports covariance and contravariance of generic interface and delegate types. We’ll consider these concepts in the context of arrays, which have always been covariant and contravariant in C#.
Recall our Employee
class hierarchy from Section 12.5, which consisted of the base class Employee
and the derived classes SalariedEmployee
, HourlyEmployee
, CommissionEmployee
and BasePlusCommissionEmployee
. Assuming the declarations
SalariedEmployee[] salariedEmployees = {
new SalariedEmployee("Bob", "Blue", "111-11-1111", 800M),
new SalariedEmployee("Rachel", "Red", "222-22-2222", 1234M) };
Employee[] employees;
we can write the following statement:
employees = salariedEmployees;
Even though the array type SalariedEmployee[]
does not derive from the array type Employee[]
, the preceding assignment is allowed because class SalariedEmployee
is a derived class of Employee
.
Similarly, suppose we have the following method, which displays the string
representation of each Employee
in its employees
array parameter:
void PrintEmployees(Employee[] employees)
We can call this method with the array of SalariedEmployee
s, as in
PrintEmployees(salariedEmployees);
and the method will correctly display the string
representation of each SalariedEmployee
object in the argument array. Assigning an array of a derived-class type to an array variable of a base-class type is an example of covariance.
Covariance works with several generic interface and delegate types, including IEnumerable<T>
. Arrays and generic collections implement the IEnumerable<T>
interface. Using the salariedEmployees
array declared previously, consider the following statement:
IEnumerable<Employee> employees = salariedEmployees;
In earlier versions of C#, this generated a compilation error. Interface IEnumerable<T>
is now covariant, so the preceding statement is allowed. If we modify method PrintEmployees
, as in
void PrintEmployees(IEnumerable<Employee> employees)
we can call PrintEmployees
with the array of SalariedEmployee
objects, because that array implements the interface IEnumerable<SalariedEmployee>
and because a Salaried-Employee
is an Employee
and because IEnumerable<T>
is covariant. Covariance like this works only with reference types that are related by a class hierarchy.
Previously, we showed that an array of a derived-class type (salariedEmployees
) can be assigned to an array variable of a base-class type (employees
). Now, consider the following statement, which has always compiled in C#:
SalariedEmployee[] salariedEmployees2 =
(SalariedEmployee[]) employees;
Based on the previous statements, we know that the Employee
array variable employees
currently refers to an array of SalariedEmployee
s. Using a cast operator to assign employees
—an array of base-class-type elements—to salariedEmployees2
—an array of derived-class-type elements—is an example of contravariance. The preceding cast will fail at runtime if employees
is not an array of SalariedEmployee
s.
To understand contravariance in generic types, consider a SortedSet
of SalariedEmployee
s. Class SortedSet<T>
maintains a set of objects in sorted order—no duplicates are allowed. The objects placed in a SortedSet
must implement the IComparable<T>
interface. For classes that do not implement this interface, you can still compare their objects using an object that implements the IComparer<T>
interface. This interface’s Compare
method compares its two arguments and returns 0 if they’re equal, a negative integer if the first object is less than the second, or a positive integer if the first object is greater than the second.
Our Employee
hierarchy classes do not implement IComparable<T>
. Let’s assume we wish to sort Employee
s by social security number. We can implement the following class to compare any two Employee
s:
class EmployeeComparer : IComparer<Employee>
{
int IComparer<Employee>.Compare(Employee a, Employee b)
{
return a.SocialSecurityNumber.CompareTo(
b.SocialSecurityNumber);
}
}
Method Compare
returns the result of comparing the two Employee
s’ social security numbers using string
method CompareTo
.
Now consider the following statement, which creates a SortedSet
:
SortedSet<SalariedEmployee> set =
new SortedSet<SalariedEmployee>(new EmployeeComparer());
When the type argument does not implement IComparable<T>
, you must supply an appropriate IComparer<T>
object to compare the objects that will be placed in the Sorted-Set
. Since, we’re creating a SortedSet
of SalariedEmployee
s, the compiler expects the IComparer<T>
object to implement the IComparer<SalariedEmployee>
. Instead, we provided an object that implements IComparer<Employee>
. The compiler allows us to provide an IComparer
for a base-class type where an IComparer
for a derived-class type is expected because interface IComparer<T>
supports contravariance.
For a list of covariant and contravariant interface types, visit
https://msdn.microsoft.com/library/dd799517#VariantList
It’s also possible to create your own types that support covariance and contravariance. For information, visit
https://msdn.microsoft.com/library/mt654058