Search code examples
c#genericsgeneric-variance

Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?


The following raises complaints:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
    IInvariant<TCov> M(); // The covariant type parameter `TCov'
                          // must be invariantly valid on
                          // `ICovariant<TCov>.M()'
}
interface IContravariant<in TCon> {
    void M(IInvariant<TCon> v); // The contravariant type parameter
                                // `TCon' must be invariantly valid
                                // on `IContravariant<TCon>.M()'
}

but I can't imagine where this wouldn't be type-safe. (snip*) Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of?


* My initial thoughts were admittedly convoluted, but despite this, the responses are very thorough, and @Theodoros Chatzigiannakis even dissected my initial assumptions with impressive accuracy.

Alongside a good slap from retrospect, I realize that I had falsely assumed that the type signature of ICovariant::M remains a Func<IInvariant<Derived>> when its ICovariant<Derived> is assigned to a ICovariant<Base>. Then, assigning that M to Func<IInvariant<Base>> would look fine coming from an ICovariant<Base>, but would of course be illegal. Why not just ban this last, obviously-illegal cast? (so I thought)

I feel this false and tangential guess detracts from the question, as Eric Lippert also points out, but for historical purposes, the snipped part:

The most intuitive explanation to me is that, taking ICovariant as an example, the covariant TCov implies that the method IInvariant<TCov> M() could be cast to some IInvariant<TSuper> M() where TSuper super TCov, which violates the invariance of TInv in IInvariant. However, this implication doesn't seem necessary: the invariance of IInvariant on TInv could easily be enforced by disallowing the cast of M.


Solution

  • I'm not sure that you actually got your question answered in either of the answers so far.

    Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?

    It doesn't, so the question is based on a false premise. The actual rules are here:

    https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/

    Consider now:

    interface IInvariant<TInv> {}
    interface ICovariant<out TCov> {
       IInvariant<TCov> M(); // Error
    }
    

    Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of?

    I'm not following your explanation, so let's just say why this is disallowed without reference to your explanation. Here, let me replace these types with some equivalent types. IInvariant<TInv> can be any type that is invariant in T, let's say ICage<TCage>:

    interface ICage<TAnimal> {
      TAnimal Remove();
      void Insert(TAnimal contents);
    }
    

    And maybe we have a type Cage<TAnimal> that implements ICage<TAnimal>.

    And let's replace ICovariant<T> with

    interface ICageFactory<out T> {
       ICage<T> MakeCage();
    }
    

    Let's implement the interface:

    class TigerCageFactory : ICageFactory<Tiger> 
    { 
      public ICage<Tiger> MakeCage() { return new Cage<Tiger>(); }
    }
    

    Everything is going so well. ICageFactory is covariant, so this is legal:

    ICageFactory<Animal> animalCageFactory = new TigerCageFactory();
    ICage<Animal> animalCage = animalCageFactory.MakeCage();
    animalCage.Insert(new Fish());
    

    And we just put a fish into a tiger cage. Every step there was perfectly legal and we ended up with a type system violation. The conclusion we reach is that it must not have been legal to make ICageFactory covariant in the first place.

    Let's look at your contravariant example; it's basically the same:

    interface ICageFiller<in T> {
       void Fill(ICage<T> cage);
    }
    
    class AnimalCageFiller : ICageFiller<Animal> {
      public void Fill(ICage<Animal> cage)
      {
        cage.Insert(new Fish());
      }
    }
    

    And now, the interface is contravariant so this is legal:

    ICageFiller<Tiger> tigerCageFiller = new AnimalCageFiller();
    tigerCageFiller.Fill(new Cage<Tiger>());
    

    Once again we have put a fish into a tiger cage. Once again we conclude that it must have been illegal to make the type contravariant in the first place.

    So now let's consider the question of how we know that these are illegal. In the first case we have

    interface ICageFactory<out T> {
       ICage<T> MakeCage();
    }
    

    And the relevant rule is:

    The return types of all non-void interface methods must be valid covariantly.

    Is ICage<T> "valid covariantly"?

    A type is valid covariantly if it is: 1) a pointer type, or a non-generic class... NOPE 2) An array type... NOPE 3) A generic type parameter type ... NOPE 4) A constructed class, struct, enum, interface or delegate type X<T1, … Tk> YES! ... If the ith type parameter was declared as invariant, then Ti must be valid invariantly.

    TAnimal was invariant in ICage<TAnimal>, So T in ICage<T> must be valid invariantly. Is it? No. To be valid invariantly it must be valid both covariantly and contravariantly, but it is valid only covariantly.

    Therefore this is an error.

    Doing the analysis for the contravariant case is left as an exercise.