4
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.
    }
}
svick
  • 236,525
  • 50
  • 385
  • 514
johansson
  • 1,467
  • 2
  • 10
  • 9
  • Did you try casting child as 'ATypeImpl'? – Daniel Wardin Mar 23 '13 at 18:40
  • How do you want to interact with the data structure once it's constructed? – dtb Mar 23 '13 at 18:51
  • I think it would be easier to understand - and quite possibly to see where it would fall down - if you used more concrete names. But I'll try to have a look later on... – Jon Skeet Mar 23 '13 at 19:58
  • I want the data structure, as in the Children, to be limited to its subtype. For example, AnimalWrapper would enforce that ISet be ISet. Thus, it only can hold Mammal or Dog. Can't hold Reptile. The only way I've done this was via reflection and was wondering if there's another way. – johansson Mar 24 '13 at 03:35

2 Answers2

2

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

NSGaga-mostly-inactive
  • 14,052
  • 3
  • 41
  • 51
  • Yep, that looks like it, similar to the original code, and the other answer, but with the 'out' on the interface using generics to fix the covariance issue. That makes sense to me, but one thing I'm still not sure about is how it actually gets enforced. Is that just because ISet is not covariant and thus you're forced to cast to whatever's actually in the set? – johansson Mar 24 '13 at 20:10
  • I'll edit some explanations into it - I'm not sure I quite understood the question - but let me try - first, best if you take out the 'out' and see what happens. Or if you make the `ISet` instead of `IEnumerable` in the `INode`. W/o `out` (which makes `INode` covariant) it won't allow you to upcast `INode` -> `INode`. Because, w/o any designation it's not allowed (The fact that `Dog : Mammal` doesn't mean much (it depends on how you use it). So, we need to make `INode<>` behave pretty much like `IEnumerable` (which is defined similarly). To do that, we need to banish any `Add`... – NSGaga-mostly-inactive Mar 25 '13 at 02:04
  • ...that accept `` as `input param` (contravariance / `in`) - which is where `ISet` gets in the way. Thankfully it's also `IEnumerable` - so we can 'trick' it by keeping `INode` clean and 'read only' and enumerating pretty much. Then the `Node` takes the responsibility of keeping the ISet and adding the children etc. However, we still get the upcasting right (keep in mind that `ISet` has to be like that (not `ISet`) which is essential. So, in short - you have two opposing concepts going on - ISet/adding ('in' param T) and upcasting - thus we have to 'split' the responsibilities. – NSGaga-mostly-inactive Mar 25 '13 at 02:09
1

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());
blins
  • 2,515
  • 21
  • 32
  • Right, but the same problem would occur if I wrote IMammalWrapper to inherit from IAnimalWrapper. Basically, I want to limit the ISet contained in the wrapper to only the type defined. – johansson Mar 24 '13 at 03:32