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.
Note. Throughout the discussion, we assume that:
- closed generic types belong to the same open generic type.
- the type arguments are of reference types.
- 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>();).
- open generic interface
- closed generic interface
- relationship between type arguments of two closed generic interfaces of the same open generic interface
- assignment compatibility
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 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");
}
}
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.");
}
}
{
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;
}
}
// 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.
No comments:
Post a Comment