9

I have a problem with returning collection and covariance and I was wondering if anyone has a better solution.

The scenario is this:

I have 2 version of implementation and I would like to keep the version implementation completely separate (even though they might have the same logic). In the implementation, I would like to return a list of items and therefore in the interface, I would return a list of interface of the item. However, in the actual implementation of the interface, I would like to return the concrete object of the item. In code, it looks something like this.

interface IItem
{
    // some properties here
}

interface IResult
{
    IList<IItem> Items { get; }
}

Then, there would be 2 namespaces which has concrete implementation of these interfaces. For example,

Namespace Version1

class Item : IItem

class Result : IResult
{
    public List<Item> Items
    {
        get { // get the list from somewhere }
    }

    IList<IItem> IResult.Items
    {
        get
        {
            // due to covariance, i have to convert it
            return this.Items.ToList<IItem>();
        }
    }
}

There will be another implementation of the same thing under namespace Version2.

To create these objects, there will be a factory that takes the version and create the appropriate concrete type as needed.

If the caller knows the exact version and does the following, the code works fine

Version1.Result result = new Version1.Result();
result.Items.Add(//something);

However, I would like the user to be able to do something like this.

IResult result = // create from factory
result.Items.Add(//something);

But, because it's been converted to another list, the add will not do anything because the item will not be added back to the original result object.

I can think of a few solutions such as:

  1. I could synchronize the two lists but that seems to be extra work to do
  2. Return IEnumerable instead of IList and add a method for create/delete collections
  3. Create a custom collection that takes the TConcrete and TInterface

I understand why this is happening (due to the type safe and all), but none of workaround I think can of seems very elegant. Does anybody have better solutions or suggestions?

Thanks in advance!

Update

After thinking about this more, I think I can do the following:

public interface ICustomCollection<TInterface> : ICollection<TInterface>
{
}

public class CustomCollection<TConcrete, TInterface> : ICustomCollection<TInterface> where TConcrete : class, TInterface
{
    public void Add(TConcrete item)
    {
        // do add
    }

    void ICustomCollection<TInterface>.Add(TInterface item)
    {
        // validate that item is TConcrete and add to the collection.
        // otherwise throw exception indicating that the add is not allowed due to incompatible type
    }

    // rest of the implementation
}

then I can have

interface IResult
{
    ICustomCollection<IItem> Items { get; }
}

then for implementation, I will have

class Result : IResult
{
    public CustomCollection<Item, IItem> Items { get; }

    ICustomCollection<TItem> IResult.Items
    {
        get { return this.Items; }
    }
}

that way, if the caller is accessing the Result class, it will go through the CustomCollection.Add(TConcrete item) which is already TConcrete. If the caller is accessing through IResult interface, it will go through customCollection.Add(TInterface item) and the validation will happen and make sure the type is actually TConcrete.

I will give it a try and see if this would work.

Khronos
  • 121
  • 2
  • 1
    I think that going with option #2 will require the least amount of code. It will also reduce the surface area of your interface, since the implementation won't be responsible for full IList support, which the callers probably don't need. – Brent M. Spell Sep 11 '11 at 17:01
  • Your solution looks good, but why do you use `ICustomCollection` and not `ICollection` directly? – svick Sep 11 '11 at 18:50
  • I totally could. The only reason is that I have a few methods that's specifically for the colleciton that's not available for the regular ICollection that needs to be accessed by internal code, which I forgot to mention. – Khronos Sep 12 '11 at 03:20

3 Answers3

2

The problem you are facing is because want to expose a type that says (among other things) “you can add any IItem to me”, but what it actually will do is “You can add only Items to me”. I think the best solution would be to actually expose IList<Item>. The code that uses this will have to know about the concrete Item anyway, if it should add them to the list.

But if you really want to do this, I think the cleanest solution would be 3., if I understand you correctly. It's going to be a wrapper around IList<TConcrete> and it will implement IList<TInterface>. If you try to put something into it that is not TConcrete, it will throw an exception.

svick
  • 236,525
  • 50
  • 385
  • 514
0

+1 to Brent's comment: return non-modifyable collection and provide modification methods on the classes.

Just to reiterate why you can't get 1 working in explainable way:

If you try to add to List<Item> element that simply implements IItem but is not of type (or derived from) Item you will not be able to store this new item in the list. As result interface's behavior will be very inconsistent - some elements that implement IItem can be added fine, sime would fail, and when you change implementation to version2 behavior will cahnge too.

Simple fix would be to store IList<IItem> insitead of List<Item>, but exposing colection directly requires careful thinking.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
0

The problem is that Microsoft uses one interface, IList<T>, to both describe both collections that can be appended to, and collections that cannot. While it would be theoretically possible for a class to implement IList<Cat> in mutable fashion and also implement IList<Animal> in immutable fashion (the former interface's ReadOnly property would return false, and the latter would return true), there's no way for a class to specify that it implements IList<Cat> one way, and IList<T> where cat:T, another way. I wish Microsoft had made IList<T> List<T> implement IReadableByIndex<out T>, IWritableByIndex<in T>, IReadWriteByIndex<T>, IAppendable<in T>, and ICountable, since those would have allowed for covariance and contravariance, but they didn't. It may be helpful to implement such interfaces yourself and define wrappers for them, depending upon the extent to which covariance would be helpful.

supercat
  • 77,689
  • 9
  • 166
  • 211