Search code examples
c#covariancecontravariance

Is nested CoVariance becomes Contravariance?


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.


Solution

  • 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, then IObserver<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, then IVable<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.