Search code examples
c#genericsinheritancecovariance

Covariance in Generics: Creating a Generic List with a Bounded Wildcard


I am looking the whole day for a proper solution, bit I am fairly new to C#. If I am right, want something similar to the Java code

ArrayList<IAnimalStuff<? extends Animal>> ianimals = new ArrayList<>();

just for C#. Or another solution when I am on the wrong way.

Detailed Scenario: I have a base class (Animal) and multiple subclasses (e.g. Dog).

class Animal
{
}
class Dog : Animal
{
}

I create a common list of all animals, which contains objects of all kinds of different animals.

List<Animal> animals = new List<Animal>();
animals.add(new Dog()); // and so on

Additionally, I have an interface and a class for each special animal derived from this interface.

interface IAnimalStuff<TAnimal> where TAnimal : Animal
{
    void doSomething(TAnimal animal);
}

public class DogStuff : IAnimalStuff<Dog>
{
    public override void doSomething(Dog animal) 
    {
    }
}

Now I want to manage one list with Animals and one list with AnimalStuff. When looping over all animals, I want to perform all Animalstuff in the other list which are valid for a dog. While a list of animals is no problem, I have my problems to create the other list.

List<IAnimalStuff<Animals>> ianimals = new List<IAnimalStuff<Animals>>();

Unlike in the first list, I only can add objects to this list of type

IAnimalStuff<Animals>

, but I also want to do

ianimals.add(GetDogStuff()); // add object of type IAnimalStuff<Dog>

I assumed this is valid, because Dog is a subclass of Animal. I think with the upper line of Java code this can be solved, but I did not find any solution for C#. Or am I on the wrong track?


Solution

  • C# has declaration-site variance, not use-site variance like Java.

    In C# you could do this:

    interface IAnimalStuff<in TAnimal> where TAnimal : Animal // note "in"
    {
        void doSomething(TAnimal animal);
    }
    

    And then you could say

    IAnimalStuff<Mammal> iasm = new MammalStuff();
    IAnimalStuff<Dog> iasd = iasm;
    

    Why does this work? Because iasm.doSomething takes any mammal, and iasd.doSomething will be passed only dogs, and dogs are mammals. Notice that this is a contravariant conversion.

    But you cannot go the other way; you can't say "dog is a mammal, therefore a dogstuff is a mammalstuff". That mammalstuff can accept a giraffe, but a dogstuff cannot. That would be a covariant conversion.