6

I've been busy with C# 4.0 generics, and now I basically want to do something like this:

public abstract class GenericTree<T> : Tree
    where T : Fruit
{
    public Tree(IFruitCollection<T> fruits)
        : base(fruits) { }
}

The base Tree class looks like this:

public abstract class Tree
{
    private IFruitCollection<Fruit> fruits;

    public IFruitCollection<Fruit> GetFruits
    {
        get { return fruits; }
    }

    public Tree(IFruitCollection<Fruit> fruits)
    {
        this.fruits = fruits;
    }
}

Here is my first problem. The constructor of GenericTree can't cast the the generic collection to a fruit collection. I've also got an implementation of the GenericTree:

public class AppleTree : GenericTree<Apple>
{
    public AppleTree()
        : base(new FruitCollection<Apple>) { }
}

Here's my second problem. When I add fruit to an instance of AppleTree, using myAppleTree.GetFruits.Add(...), I am not restricted to apples only. I am allowed to add all kinds of fruit. I don't want this.

I tried to solve that problem by adding this to the GenericTree:

new public IFruitCollection<T> GetFruits
{
    get { return base.GetFruits as IFruitCollection<T>; }
}

But this is not possible either. It somehow always returns null. It could be possible that this will be solved, when my first problem is solved.

The IFruitCollection interface looks like this:

public interface IFruitCollection<T> : ICollection<T>
    where T : Fruit { ... }

And the FruitCollection class is a simple implementation of the Collection class. Oh, and of course, the Apple class extends the Fruit class.

The solution is to make the IFruitCollection interface compatible with both covariance and contravariance. But how do I achieve this? The "in" or "out" parameter keywords are not possible, because the ICollection interface doesn't allow it.

Thanks a lot for your help!

Rudey
  • 4,717
  • 4
  • 42
  • 84
  • Please note that this can easily be solved by merging the Tree and the GenericTree class into one. But remember that this is only an example of what my problem is. My own application is a little more complex. – Rudey Feb 02 '12 at 16:07
  • As you imply, the solution is to make Tree generic. You also imply that this isn't possible. Why not? – phoog Feb 05 '12 at 03:41
  • At some point, I need a list of Trees. And a list of Tree doesn't do it, because doesn't allow me to add Tree instances. It will than of course say that there is no implicit reference between Tree and Tree. – Rudey Feb 07 '12 at 09:33

2 Answers2

3

In response to your comment:

At some point, I need a list of Trees. And a list of Tree<Fruit> doesn't do it, because doesn't allow me to add Tree<Banana> instances. It will than of course say that there is no implicit reference between Tree<Fruit> and Tree<Banana>.

The basic problem is that you want to have a collection -- the list of trees -- that (indirectly) contains similar objects of different types. For the sake of disambiguating that collection from the Tree (which is also a collection, of Fruit), let's call it an Orchard.

Orchard -(contains)-> Tree -(contains)-> Fruit

If you make a non-generic Tree, the Orchard's elements could all be of that type. But as you noticed, this means that you end up with the problem that the trees are not type safe, and you can put a banana in an apple tree. You would have to solve that problem with runtime type checks in the Tree implementation.

Alternatively, you could make a generic Tree<T> where T : Fruit class, so you have type safety with respect to the subclass of Fruit contained in the tree. This means that the Orchard will contain objects of different types, again requiring the need for runtime type checks.

(You could make an Orchard with static type safety by declaring a separate accessor for each type:

class Tree { }
class Tree<T> : Tree { }
class Trees : IEnumerable<Tree>
{
    Tree<Banana> _bananaTree;
    Tree<Apple> _appleTree;
    //...etc.

    Tree<Banana> GetBananaTree() { return _bananaTree; }
    Tree<Apple> GetBananaTree() { return _appleTree; }
    //...etc.

    public IEnumerator<Tree> GetEnumerator()
    {
        yield return _bananaTree;
        yield return _appleTree;
        //...etc.
    }
}

But that is probably not what you want, so you need to have a cast somewhere.)

I assume that you'd rather have the cast in the Orchard than in the Tree, in which case I would suggest something like this for the underlying approach:

IDictionary<Type, object> orchard = new Dictionary<Type, object>();

//to retrieve the tree
Tree<Banana> bananaTree = (Tree<Banana>)orchard[typeof(Banana)];

(Of course, you could use a more specific type than object, like Tree, or ICollection, or IEnumerable.)

I would go further and encapsulate that logic in an Orchard class, which could provide a less verbose syntax by performing the cast in the accessor:

var bananaTree = orchard.GetTree<Banana>();

where:

public class Orchard
{
    private IDictionary<Type, object> _trees;

    //...

    public Tree<T> GetTree<T>()
    {
        return (Tree<T>)_trees[typeof(T)];
    }
}

Ultimately, this is a great example of why co- and contravariant type parameters in interfaces must be restricted to output and input positions, respectively. For types that are used both as input and output, you wind up needing runtime type checks.

phoog
  • 42,068
  • 6
  • 79
  • 117
  • Thanks for your reply. Yesterday I realised that I need to get rid of the non-generic Tree class, and you practically confirmed that for me. Your Orchard class seems like a great workaround. I'm afraid though that this will not be applicable for my 'real' application, I will try it out tomorrow. – Rudey Feb 07 '12 at 21:59
  • Your workaround did work for me, but I found another workaround while using yours. Take a look at my own answer. Thanks for your help though! – Rudey Feb 09 '12 at 17:12
  • @Rudey that's an interesting solution, and it will work fine if you don't have tons of objects. I've got a project where the collections contain millions of objects, and the storage overhead of having *two* collections for differently-typed references would be unacceptable there. – phoog Feb 09 '12 at 18:20
  • @Rudey on second thought, I don't see what this gets you. What type is the list that you store these objects in? Is it a `List`? If so, you still have the problem that when you retrieve the `GenericTree` from that list, the reference is of type `Tree`, and the compiler can't stop you from adding a `Banana` to it. You still need a run-time type check. – phoog Feb 09 '12 at 18:35
  • You're right. But my List isn't used for adding fruit, so it doesn't matter what kind of fruit tree it is. In my case fruit will only be added in that tree's constructor, via this.Fruits.Add(...). If I intended to use that list for adding fruit, I would definitely use your solution. I guess I'll be more careful next time when translating my problem into a simple case. – Rudey Feb 09 '12 at 21:02
  • and of course there are some other occurrences when I add fruit, else I wouldn't use generics. And about the storage problem, I'm not cloning each object, so how would that use more storage? Though there are two different references, aren't they still pointing to the same object? – Rudey Feb 09 '12 at 21:10
  • @Rudey if you're not adding fruit to the trees from the list, what are you doing? If you're just enumerating them, for example, (or using their elements in any other output position), you can just rely on interface covariance and make the list a list of `IEnumerable` (or whatever covariant interface exposes the output position you want to use). – phoog Feb 10 '12 at 00:03
  • @Rudey FruitCollection both inherits from FruitCollection and decorates a wrapped instance of FruitCollection. The wrapped collection is not identical to its wrapper, because you pass in an existing wrapped collection (base.Fruits) when you instantiate the wrapper. Therefore, if FruitCollection stores its elements in arrays, you have two arrays storing each element. To test this, try `ReferenceEquals(this, this.baseCollection)` somewhere inside `FruitCollection`. – phoog Feb 10 '12 at 00:07
  • @Rudey think about it: if you need to use events to keep the list's memberships synchronized, then they must not be the same list, right? – phoog Feb 10 '12 at 00:12
  • Oooh, I now see what you mean. Indeed, having 2 collections for every fruit tree does take more storage. But remember that the elements inside those collections are the same, since they all return true when I check them with ReferenceEquals(this[i], this.baseCollection[i]) inside FruitCollection. – Rudey Feb 11 '12 at 13:09
1

I think I found my solution, while I was trying out phoog's workaround. Thanks, phoog!

At first I didn't give GenericTree his own FruitCollection. Mosty because the base class often loops through his own collection, for example to update the fruits. This resulted into fruits not updating, because they were added to the GenericTree's collection, not to the base collection.

But eventually I realised that casting those collections is never going to work either.

Now I've made another FruitCollection class that automatically adds and removes components to the base FruitCollection, and vice versa. This solution works great for me!

public FruitCollection<T, U> : FruitCollection<T>
    where T : U
    where U : Fruit
{
    private FruitCollection<U> baseCollection;

    public FruitCollection(FruitCollection<U> baseCollection)
        : base()
    {
        this.baseCollection = baseCollection;

        // here I added code that throws events whenever the collection is changed
        // I used those events to add/remove fruit to the base collection, and vice versa
        // see the link below.
        ...
    }

    ...
}

public class GenericTree<T>: Tree
    where T : Fruit
{
    private FruitCollection<T> fruits;

    // use the new keyword to hide the base collection
    new public FruiCollection<T> Fruits
    {
        get { return fruits; }
    }

    public GenericTree()
        : base()
    {
        // after hiding the base collection, use base.Fruits to get it
        fruits = new FruitCollection<T, Fruit>(base.Fruits);
    }
}

This helped me: How to handle add to list event?

Community
  • 1
  • 1
Rudey
  • 4,717
  • 4
  • 42
  • 84