I understood the Concepts of CoVariance and CotraVariance as, when ever an Interface need to be marked as Read-only then we use out T and for Write-only interface we use in T so that these make sure the Type Safety over getting and setting things of an Object. But when we nest these how does ContraVariance is treated as CoVariance. For Example:
interface IObservable<out T>
{
IDisposable Subscribe(IObserver<T> o);
}
interface IObserver<in T>
{
void OnNext(T t);
void OnError(Exception e);
void OnCompleted();
}
Please explain with an example how nested things change their variance. or Please redirect me to a good Reading Material. Thanks in advance.
Let us write ⊂
(a modified kind of less-than symbol) for the "is a" relation we have for reference types. For example Elephant ⊂ IMammal
, or IMammal ⊂ IAnimal
.
Look at IObserver<>
first:
interface IObserver<in T>
{
void OnNext(T t);
// other members irrelevant
}
The "in" means it is contravariant. Contravariant means:
If
X ⊂ Y
, thenIObserver<Y> ⊂ IObserver<X>
.
The contra- is because the order of X
and Y
changes when you "apply IObserver<·>
on both sides of the ⊂
". That is similar to multiplying an inequality (from math) by a negative number on both sides. You have to swap the two sides of the inequality (or turn the ⊂
into a ⊃
).
The reason why it is allowed to have IObserver<T>
contravariant in T
is because T
is only used for a value parameter in a method (namely OnNext
).
Suppose we have an instance of IObserver<IMammal>
. This can take in any IMammal
in its OnNext(IMammal t)
method. Can this instance act as an IObserver<Elephant>
also? Yes, because if it can take in any IMmammal
, then in particular it can take in an Elephant
, and so the contravariance is safe and sound. Because Elephant
"is an" IMammal
, then IObserver<IMammal>
"is an" IObserver<Elephant>
.
The contravariance reverses the relation.
Now, let us look at the other type (and now we come to what you are asking!). I rename it from IObservable<>
to IVable<>
:
interface IVable<out T>
{
IDisposable Subscribe(IObserver<T> o);
}
The "out" means covariant. Covariant means:
If
X ⊂ Y
, thenIVable<X> ⊂ IVable<Y>
.
This is like in math when you multiply both sides of an inequality by a positive number. X
and Y
are not swapped (co-).
But how can IVable<T>
be covariant, when it has a method, Subscribe
, that takes in "something with T
"? That is because that "something" is contravariant in T
!
Let us see if it is safe and sound. So, suppose we have an instance which is IVable<IMammal>
. This means that it has a Subscribe
which can take in any IObserver<IMammal>
. But the latter is contravariant, so because an IMammal
"is an" IAnimal
, then any IObserver<IAnimal>
"is an" IObserver<IMammal>
. So this shows us that our IVable<IMammal>
can act as an IVable<IAnimal>
as indicated by covariance. So all is good.
In conclusion: Taking "in" something which is contravariant in T
, acts like "out" in T
. Similarly, sending "out" something which is contravariant in T
, act like "in" in T
.