简体   繁体   中英

How to make generic class that contains a Set of only its own type or subtypes as Children?

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.
    }
}

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

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());

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM