On Csharp - Covariance and Contravariance
Created on: 12 Jun 24 21:53 +0700 by Son Nguyen Hoang in English
Useful concepts for generic interface, delegates and more!
Recently I decided to teach myself some C# knowledge that I found my self lacking, including but not limited to Task
(Concurrency
), delegates
and some Design Patterns. While reading books, I encountered two concepts that sound pretty weird. They are Covariance and Contravariance. What are these?
Problem
- I didn’t find any good translation to Vietnamese of these two concepts.
- The first result from Google indicates that Covariance is related to statistic?
- So, what are they?
Definition
Below here my own definition for some keywords:
- Deprived Type (from something): A data type that deprived (or inherited) from another type. E.g:
string
is deprived fromobject
- Base Type (of something): A data type from which another type inheriting from. E.g
object
is basetype ofstring
- Variance (to something): Having relation to another type (either as deprived type or base type)
- Covariance: In English, prefix Co indicates mutual, join, or commons. In this context, contravariance is the ability to use subtype to the place supposed to belong in the base type
- Contravariance: In English, prefix Contra indicates “opposite”. In this context, contravariance is the ability to use base type to the place supposed to belong in the deprived type.
- Extra: Invariance indicates that the two type has no relations whatsoever.
We also provide a scenario to illustrate two types. Imagine there is a relation between two type: Animal
and Dog
. In which, Dog
inherited from Animal
.
Given these concepts predefined, let’s go to the details.
Special Keyword: in/out
-
in
andout
, has two meanings in c#:- The first situation you encountered them are for function parameters.
- The second situation you encoutered them are for
generic interface
andgeneric delegates
-
Here, we focus on the later meaning. In the context of function,
in
andout
implied the flow of information in respect to the function. -
In the simplest example, we have
Animal GetAnimal(string name){}
in whichAnimal
is the output, andstring
as input.
Covariance
Covariance implies the ability use a deprived type (subtype) to a place that supposed to be used for the basetype.
As it is an “ability”, not every code can have this. Array and some interface such as IEnumerable<T>
support covariance. You yourself can craft your own generic interfaces that support Covariance using keyword out
. Beside generic interfaces
, other place you can use covariance
(and contravariance
) are delegate
For array, the most simple example could be
Animal[] animals = new Animal[10]{};
animals[0] = new Dog(); //valid
On Generic Interface
, lets define some interface
for better illustration.
public interface IAct<T> {
public void DoSomething();
}
public AnimalImp : IAct<Animal> { //implements}
public DogImp : IAct<Animal> { //implements}
//Main Function:
IAct<Animal> iAnimal = new AnimalImp();
IAct<Dog> iDog = new DogImp();
The question is, can iAnimal
assigned to iDog
?
iAnimal = iDog; // Is this valid?
The answer is no. Because your generic interface is not covariant yet. To fix this, modify the IAct<>
to be
public interface IAct<out T> {
public void DoSomething();
}
iAnimal = iDog; // now this is valid
The keyword out
also indicates one more thing: You cannot use T
as the input in any function declared inside IAct
. Basically it means
public interface IAct<out T> {
public void DoSomething(T t); // this is not allowed
}
Why the rule?
Eric Lippert, one of developers who make compiler for C# gives this wonderful example
interface IR<out T>
{
void D(T t); //assuming this is valid
}
class C : IR<Mammal>
{
public void D(Mammal m)
{
m.GrowHair();
}
}
...
IR<Animal> x = new C(); // legal because T is covariant and Mammal is convertible to Animal
x.D(new Fish()); // legal because IR<Animal>.D takes an Animal
You can visit here to check his original answer on Stackoverflown.
Another example
In C#, one of the popular interface that leverages Covariance
are IEnumerable<T>
In reallity, give you check the Definition
of that interface, here is what it shows:
namespace System.Collections.Generic
{
//
// Summary:
// Exposes the enumerator, which supports a simple iteration over a collection of
// a specified type.
//
// Type parameters:
// T:
// The type of objects to enumerate.
public interface IEnumerable<out T> : IEnumerable
{
//
// Summary:
// Returns an enumerator that iterates through the collection.
//
// Returns:
// An enumerator that can be used to iterate through the collection.
IEnumerator<T> GetEnumerator();
}
}
Behind the sugar-syntax is an out
keyword under the hood!
Contravariance
Contravariance is opposite ("contra") to Covariance that it allowed the less specific type (Animal
) to the place that is supposed for specific type Dog
. Similar to Covariance, generic interface that supports contravariance go with special keyword in
.
public interface IAct<in T> {
public void DoSomething(T instance);
}
public AnimalImp : IAct<Animal> { //implements}
public DogImp : IAct<Animal> { //implements}
As we have in
in the interface, below code are valid:
IAct<Animal> animal = new AnimalImp();
IAct<Dog> dog = new DogImp();
dog = animal //This is valid!
T
in contravariance cannot in the output position when in
keyword comes with T
. Why this rule is enforced?
Why the rule?
Similar to Eric’s example, here we have another example to illustrate.
interface IR<in T>
{
T D( t); //assuming this is valid
}
class Fish : Animal {}
class Mammal : Animal {}
class C : IR<Animal>
{
public Animal D()
{
return new Mammal(); // valid because Mammal inherited from Animal
}
}
IR<Fish> fish = new C(); //valid? But now Fish having a Mammal instance inside
Another example
In C#, one of the popular interface that leverages Contravariance
are IComparer<T>
. Similar to the IEnumerable<T>
, I provide the definition of interface in c#:
namespace System.Collections.Generic
{
//
// Summary:
// Defines a method that a type implements to compare two objects.
//
// Type parameters:
// T:
// The type of objects to compare.
public interface IComparer<in T>
{
// comment has been discarded
int Compare(T? x, T? y);
}
}
You can find the in
keyword!
Extra.
Below declarations are totally valid:
public interface IMyInterface<in T, in T1, out T2, out T3 ...> //this is valid
So you can mix-match the output/input for the function as you wish!
public void Test1(T1 t1);
public T2 void Test2(T1 t1, T t);
Further Discussion
Support both Contravariance and Covariance on same Type?
- It could be wild to think an interface that support both Contravariance and Covariance in a type.
- Assuming this exist we would have:
IAct<Animal> animal = new AnimalImp();
IAct<Dog> dog = new DogImp();
IAct<Fish> fish = new FishImp();
// If this is allowed, then:
dog = fish
What exactly we would have here? We basically make a feature that allow any type to be convertible to any other type as long as they have same basetype.
Apply this thinking to IAct<object>
, IAct<Exception>
and IAct<string>
. This would means IAct<string>
can be converted to IAct<Exception>
Again, Eric Lippert answered the question here.
What about Covariance and Contravariance on Generic Class?
Assuming you can do this
public class Animal<in T> {
T CreateT() // this would violate the keyword "in"
}
Basically, if this possible, you cannot create any getter for T
in Animal
. Is this a good thing?
One more time, Eric Lippert answered this concern here.
Bonus: On Delegate
This is not supposed to be a post on delegate, but unfortunately the topic of Covariance and Contravariance related to delegate
so much, as in
and out
are used extensively in delegate and other built-in delegate type like Action
and Func
.
Covariance on delegate
The code below are taken from here
// Type T is declared covariant by using the out keyword.
public delegate T SampleGenericDelegate <out T>();
public static void Test()
{
SampleGenericDelegate <String> dString = () => " ";
// You can assign delegates to each other,
// because the type T is declared covariant.
SampleGenericDelegate <Object> dObject = dString; //valid
}
As in generic interface
rule, T
cannot in input position.
Contravariance on delegate
We can easily craft a similar example
public delegate void NewSampleGenericDelegate<in T>();
public static void Test()
{
NewSampleGenericDelegate<string> sDelegate = delegate() {
// Do Nothing here
};
NewSampleGenericDelegate<object> objDelegate = delegate() {
// Do Nothing here
};
sDelegate = objDelegate; // valid
}
As in `generic interface` rule, `T` cannot in *output* position.
Action and Func
Behind the scene, Action
and Func
are basically delegate
under sugar syntax. The difference is that Action
return value while Func
are not. They are basically predefined delegate
.
Checking the definition of Action
from this link we have:
public delegate void Action<in T>(T obj);
public delegate void Action<in T1,in T2>(T1 arg1, T2 arg2);
...
// It support at most 16 parameters.
Because they are basically delegate with in
, Action
supports contravariance:
public static Action<Dog> MyAction;
public static void Demo2(){
void H(Animal animal){
// do something
}
MyAction = H; // Valid
}
In the addition, under the hood of Func
there is something similar:
public delegate TResult Func<in T,out TResult>(T arg);
public delegate TResult Func<in T1,in T2,out TResult>(T1 arg1, T2 arg2);
...
// It support at most 16 parameters for input and 1 parameter for output.
To illustrate covariance and contravariance for Func
, we provide some example:
static Func<string, Animal> myFunc;
public static void Main3()
{
Dog CreateDog1(string s) => new Dog();
Dog CreateDog2(object s) => new Dog();
myFunc = CreateDog1; // valid
myFunc = CreateDog2; // valid
}
End
That’s it for my short analysis. This is the first time I encountered this concept so misunderstanding can happens. If this article contains any error then please let me know.
Best Regards.
P/S: If you are Vietnamese and you know good translations for these two concepts then please let me know.