3

I'm trying to setup a CoR with interfaces where a handler in the chain can be a for a less derived event type using contravariance. I create this interface to do it.

public interface IHandler<in TEvent>
{
    void SetNext(IHandler<TEvent> next);
    void Handle(TEvent event);
}

It fails with the compiler error

CS1961 Invalid variance: The type parameter 'TEvent' must be covariantly valid on 'IHandler.SetNext(IHandler)'. 'TEvent' is contravariant.

I thought this should be valid since IHandler is covariant in TEvent. What am I missing?

just.another.programmer
  • 8,579
  • 8
  • 51
  • 90
  • Also relevant: https://stackoverflow.com/questions/48177452/covariance-and-contravariance-with-func-in-generics/48177835#48177835 and https://stackoverflow.com/questions/66401278/contravariance-invalid-when-using-interfaces-delegate-as-a-parameter-type/66402759#66402759 – Charlieface Jan 09 '22 at 13:22

1 Answers1

4

It makes a lot of sense if you think in terms of Animals and Dogs.

Let's make up some concrete implementations first:

class AnimalHandler: IHandler<Animal> { 
    public void SetNext(IHandler<Animal> next) {
        // animal handlers can handle cats :)
        next.Handle(new Cat());
    }

    // ...
}

class DogHandler: IHandler<Dog> { ... }

Since the type parameter of IHandler is contravariant, a IHandler<Animal> is a kind of IHandler<Dog>:

IHandler<Animal> someAnimalHandler = new AnimalHandler();
IHandler<Dog> dogHandler = someAnimalHandler;

Suppose that your SetNext method is allowed to be declared in IHandler, then I can call SetNext with another IHandler<Dog> on this dogHandler, since that is exactly the type that dogHandler.SetNext accepts.

IHandler<Dog> anotherDogHandler = new DogHandler();
dogHandler.SetNext(anotherDogHandler);

But that shouldn't be possible! After all, dogHandler actually stores an instance of AnimalHandler, whose SetNext method accepts IHandler<Animal>, and a IHandler<Dog> is not necessarily a IHandler<Animal>, is it? Imagine what would happen if the implementation of AnimalHandler.SetNext actually runs - keep in mind that the next parameter is a DogHandler - we would be asking a dog handler to handle a cat!

Therefore, it is invalid to declare SetNext with a contravariant TEvent.

This is specified more precisely here. It is said that the type IHandler<TEvent> is input-unsafe, and so is prohibited as a parameter.

TEvent would have to be invariant for both of your interface methods to compile.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    Another way of putting it is that each level of indirection causes the variance to flip, so `void SetNext(IHandler>)` *would* be valid, see fiddle https://dotnetfiddle.net/stEU4Q – Charlieface Jan 09 '22 at 13:19
  • I don't understand where the problem actually comes in. In your example, if I call `dogHandler.SetNext(anotherDogHandler)`, any calls `dogHandler` makes on `anotherDogHandler` would be through the `IHandler` interface - meaning they would be required to pass an instance of `Dog` to the methods. Since `dogHandler` is a `IHandler` shouldn't it always be interacting with `Dog` objects? – just.another.programmer Jan 09 '22 at 18:08
  • @just.another.programmer The problem is that the _implementation_ of `dogHandler.SetNext` only takes `IHandler`, and `anotherDogHandler` is not a kind of `IHandler`. Maybe I shouldn't have omitted the implementation with "...". I have edited to make it more concrete, so you can see exactly what bad things would happen if this were to be allowed. – Sweeper Jan 09 '22 at 18:23