Roman Duzynski
Roman Duzynski

Reputation: 131

Generic Factory problem, runtime casting and co/contravariant issue

I work as a developer for few years but seems like I still do not understand some advanced stuff about generics.

I've prepared some self explanatory piece of code:

public enum AnimalType { Cat, Dog}
public interface IAnimal { }
public class Dog : IAnimal { }
public class Cat : IAnimal { }

public interface IAnimalHandler<in TAnimal> where TAnimal : IAnimal { void Handle(TAnimal animal); }
public class CatHandler : IAnimalHandler<Cat>
{
    public void Handle(Cat animal) { }
}
public class DogHandler : IAnimalHandler<Dog>
{
    public void Handle(Dog animal) { }
}

public class AnimalFactory
{
    public IAnimal GetAnimal(AnimalType type) { return type == AnimalType.Cat ? (IAnimal) new Cat() : (IAnimal)new Dog();}
}
public class AnimalHandlerFactory
{
    public IAnimalHandler<TAnimal> GetAnimalHandler<TAnimal>(TAnimal animal) where TAnimal : IAnimal
    {
        switch (animal)
        {
            case Cat cat:
                return new CatHandler() as IAnimalHandler<TAnimal>;
            case Dog dog:
                return new CatHandler() as IAnimalHandler<TAnimal>;
            default:
                throw new Exception();
        }
    }
}

public class FactoryTests
{
    [Fact]
    public async Task factory_returns_concrete()
    {
        var myAnimal = new AnimalFactory().GetAnimal(AnimalType.Dog);

        var handlerForMyAnimal = new AnimalHandlerFactory().GetAnimalHandler(myAnimal);

        Assert.NotNull(handlerForMyAnimal);
    }
}

And now, coud you please answer to my doubts?

1) Why do I have to perform casting in GetAnimalHandler() method?

2) Test is failing because AnimalFactory returns an abstraction object (declaration) and this type is used as generic type of GetAnimalHandler() method so obviously casting returned null. The question is how to solve this problem? Am I missing some pattern?

Upvotes: 2

Views: 203

Answers (1)

Rafal
Rafal

Reputation: 12619

you are correct that you are missing something about co/contravariant so to have definitions near: (source)

Covariance

Enables you to use a more derived type than originally specified.

You can assign an instance of IEnumerable< Derived> to a variable of type IEnumerable< Base>.

Contravariance

Enables you to use a more generic (less derived) type than originally specified.

You can assign an instance of Action< Base> to a variable of type Action< Derived>.

You have your animal handler defined with in and this makes it contravariant. So with your definition it is legal to

IAnimalHandler<PersianCat> handler = new CatHandler();

given

public class PersianCat : Cat { }

and if you think about it, it makes sense if you are a cat handler than you can handle Persian cats. It makes no difference to you what kind of cat you are handling you can handle them all.

The case you are trying to force is not legal:

IAnimalHandler<IAnimal> handler = new CatHandler();

using my Persian cat (and dedicated handler) it would look like this:

IAnimalHandler<Cat> handler = new PersianCatHandler();

So we have dedicated handler that can handle only Persian cats so we cannot give him just a random cat he would not know what to do with it.

So in the legal case and the contravariance definition above the more generic type is on the right hand of assignment the CatHandler (or IAnimalHandler<Cat>) and it is assigned to less generic (or more specific) variable of type IAnimalHandler<PersianCat>.

Upvotes: 1

Related Questions