Question

abstract class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }

class Reptile : Animal { }

class AnimalWrapper<T> where T : Animal
{
    public ISet<AnimalWrapper<T>> Children { get; set; }
}

class Program
{
    public static void Main(string[] args)
    {
        var foo = new AnimalWrapper<Mammal>();
        foo.Children = new HashSet<AnimalWrapper<Mammal>>();

        var child = new AnimalWrapper<Dog>();
        foo.Children.Add(child);
    }
}

This obviously doesn't compile because of foo.Children.Add(child);

I'm not sure if the above code is the most clear way to demonstrate what I want to do, so I will try to explain in plain English:

I want the ability to have a class whose Children objects are in an ISet of the same generic type. Thus, if I also had var child = new AnimalWrapper<Reptile>(); it would, at compile time, fail to do foo.Children.Add(child); because Reptile is not and does not inherit from Mammal. However, obviously, even if it's derived, as shown above, it doesn't work.

Ultimately, it'd be nice to be able to say ISet<AnimalWrapper<Animal>> baz = new HashSet<AnimalWrapper<Animal>>(); then add a new AnimalWrapper<Mammal>() to that set, and new AnimalWrapper<Reptile>() to the same set. And their children would have a property Children that's an ISet<AnimalWrapper<T>> where it's of its own type, in a way, as described above.

Is there any way or am I just expecting too much from C#? Heck I'm confusing myself. :)

Edit: Ok, so I almost figured this out, without AnimalWrapper, but with a base IAnimal interface, it could almost work:

interface IAnimal { }

abstract class Animal<T> : IAnimal where T : Animal<T>
{
    public ISet<T> Children { get; set; }
}

class Mammal : Animal<Mammal> { }

class Dog : Mammal { }

class Reptile : Animal<Reptile> { }

class Frog : Reptile { }

class Program
{
    public static void Main(string[] args)
    {
        var animals = new HashSet<IAnimal>(); // any animal can be in this
        var mammal = new Mammal();
        animals.Add(mammal);
        mammal.Children = new HashSet<Mammal>();
        var dog = new Dog();
        mammal.Children.Add(dog); // ok! a dog is a mammal
        dog.Children = new HashSet<Dog>(); // in theory, OK, but compile time error
        // because Dog : Mammal, and Mammal defines Animal<Mammal>, therefore Dog's
        // Children is actually ISet<Mammal>, rather than ISet<Dog> (which is what
        // I want, recursively apply the T in Animal.
        Mammal mammal2 = new Mammal();
        dog.Children.Add(mammal2); // should be verboten, but is allowed for the
        // same reason above.
    }
}
Was it helpful?

Solution

The main problem is, a bit oversimplified, in covariance upcasting (and contravariance with the ISet)

Try it this way...

abstract class Animal { }
class Mammal : Animal { }
class Dog : Mammal { }
class Reptile : Animal { }

interface INode<out T> where T : Animal
{
    T MySelf { get; }
    IEnumerable<INode<T>> Children { get; }
}

class Node<T> : INode<T>
    where T : Animal
{
    public Node() { this.Children = new HashSet<INode<T>>(); }
    public T MySelf { get; set; }
    public ISet<INode<T>> Children { get; set; }
    IEnumerable<INode<T>> INode<T>.Children { get { return this.Children; } }
}

class Program
{
    static void Main(string[] args)
    {
        // this is a 'typical' setup - to test compiler 'denial' for the Reptile type...

        Node<Mammal> tree = new Node<Mammal>();
        tree.MySelf = new Mammal();

        var node1 = new Node<Mammal>();
        tree.Children.Add(node1);

        var node2 = new Node<Dog>();
        tree.Children.Add(node2);

        var node3 = new Node<Reptile>();
        // tree.Children.Add(node3); // this fails to compile


        // ...and similar just more 'open' - if you 'collect' animals, all are welcome

        Node<Animal> animals = new Node<Animal>();
        animals.MySelf = new Mammal();

        INode<Mammal> mamals = new Node<Mammal>();
        animals.Children.Add(mamals);

        var dogs = new Node<Dog>();
        animals.Children.Add(dogs);

        INode<Animal> reptiles = new Node<Reptile>();
        animals.Children.Add(reptiles);
    }
}

(look up the comments)

This doesn't mean it'd work in your real-life case - as this requires some 'design refactoring' to keep it working with a more complex structure (if possible).

...just fast, I'll try to explain some more later if needed

OTHER TIPS

This happens because when you instantiate an instance of AnimalWrapper<T> using the generic type argument Mammal, the Children member will be of type ISet<AnimalWrapper<Mammal>> and not of type ISet<AnimalWrapper<Dog>>. Hence the reason you can't add an instance of AnimalWrapper<Dog>to the generic collection.

One possible way I see you could address this might be if you were to implement an interface.

interface IAnimalWrapper { }

class AnimalWrapper<T> : IAnimalWrapper where T : Animal
{
    public ISet<IAnimalWrapper> Children { get; set; }
}

Then you will need to change the way you instantiate the Children collection...

foo.Children = new HashSet<IAnimalWrapper>();

Now you can add to the different types of children...

foo.Children.Add(new AnimalWrapper<Mammal>());
foo.Children.Add(new AnimalWrapper<Dog>());
foo.Children.Add(new AnimalWrapper<Reptile>());

So that will get it to compile but I am still curious as to why you really need the generic class (AnimalWrapper<T>). I imagine there could be reasons for it but maybe just doing away with that type would simplify things (depending on the larger context)...

abstract class AnimalWithChildren
{
    public ISet<AnimalWithChildren> Children { get; set; }
}
class Mammal : AnimalWithChildren { }
class Dog : Mammal { }
class Reptile : AnimalWithChildren { }

In other words, just rely ISet<T> alone to provide the type...

var foo = new Mammal();
foo.Children = new HashSet<AnimalWithChildren>();
foo.Children.Add(new Mammal());
foo.Children.Add(new Dog());
foo.Children.Add(new Reptile());
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top