PMF
PMF

Reputation: 17248

Efficiently return IList<Interface> from List<T> (avoid casting from List<T> to List<I>)

I have the following code:

public interface ISomeObject
{
     IList<ISomeObject> Objects { get; }
}
public class SomeObject : ISomeObject
{
    public SomeObject()
    {
        Objects = new List<SomeObject>();
    }
    public List<SomeObject> Objects
    {
         get;
         set;
    }
    IList<ISomeObject> ISomeObject.Objects
    {    
        get 
        {
            // What to do here?
            // return Objects; // This doesn't work
            return Objects.Cast<ISomeObject>().ToList(); // Works, but creates a copy each time.
         }
    }

SomeObject has a public property Objects that returns a List of class type. Clients knowing that class type can use that to do whatever they want. Clients only knowing about ISomeObject can use the Objects property only to get an IList<ISomeObject>. Because it is not allowed to cast List<SomeObject> to IList<ISomeObject> (due to the apple and banana issue) I need a way of converting that. The default way, using a Cast.ToList() works, but has the downside that it creates a new List each time the property is evaluated, which may be expensive. Changing ISomeObject.Objects to return an IEnumerable<ISomeObject> has the other downside that the client can't use indexing any more (which is quite relevant in my use case). And using Linq's ElementAt() call repeatedly is expensive, when used on an IEnumerable.

Has anybody got an idea on how to avoid either problem? (of course, making SomeObject known everywhere is not an option).

Upvotes: 2

Views: 441

Answers (3)

xanatos
xanatos

Reputation: 111870

You could/should implement a class similar to ReadOnlyCollection<T> to act as a proxy. Considering that it would be read only, it could be "covariant" (not language-side, but logically, meaning that it could proxy a TDest that is a subclass/interface of TSource) and then throw NotSupportedException() for all the write methods.

Something like this (code untested):

public class CovariantReadOlyList<TSource, TDest> : IList<TDest>, IReadOnlyList<TDest> where TSource : class, TDest
{
    private readonly IList<TSource> source;

    public CovariantReadOlyList(IList<TSource> source)
    {
        this.source = source;
    }

    public TDest this[int index] { get => source[index]; set => throw new NotSupportedException(); }

    public int Count => source.Count;

    public bool IsReadOnly => true;

    public void Add(TDest item) => throw new NotSupportedException();

    public void Clear() => throw new NotSupportedException();

    public bool Contains(TDest item) => IndexOf(item) != -1;

    public void CopyTo(TDest[] array, int arrayIndex)
    {
        // Using the nuget package System.Runtime.CompilerServices.Unsafe
        // source.CopyTo(Unsafe.As<TSource[]>(array), arrayIndex);
        // We love to play with fire :-)

        foreach (TSource ele in source)
        {
            array[arrayIndex] = ele;
            arrayIndex++;
        }
    }

    public IEnumerator<TDest> GetEnumerator() => ((IEnumerable<TDest>)source).GetEnumerator();

    public int IndexOf(TDest item)
    {
        TSource item2 = item as TSource;

        if (ReferenceEquals(item2, null) && !ReferenceEquals(item, null))
        {
            return -1;
        }

        return source.IndexOf(item2);
    }

    public void Insert(int index, TDest item)
    {
        throw new NotSupportedException();
    }

    public bool Remove(TDest item)
    {
        throw new NotSupportedException();
    }

    public void RemoveAt(int index)
    {
        throw new NotSupportedException();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Use it like:

IList<string> strs = new List<string>();
IList<object> objs = new CovariantReadOlyList<string, object>(strs);

Upvotes: 4

Uladz
Uladz

Reputation: 1978

You may want try to encapsulate your List<SomeObject> making it an implementation detail and return IReadOnlyList<SomeObject> instead. Then SomeObject to ISomeObject cast want be unnecessary in interface implementation as well due to IReadOnlyList variance — you'll be able to return your Objects as IReadOnlyList<ISomeObject> .

Then just add some operations to mutate your underlying list like Add or Remove to container type if those are required.

Also I should mention that interfaces are not so good for restriction — evil consumer can easily cast your ISomeObject to SomeObject and do everything he wants, probably, you should reconsider your design. You'd better stick to such things as immutability and encapsulation for providing usable api. Explicitly use mutable builders then for immutable classes where it's reasonable.

Upvotes: 0

user743382
user743382

Reputation:

Changing ISomeObject.Objects to return an IEnumerable<ISomeObject> has the other downside that the client can't use indexing any more (which is quite relevant in my use case).

Indexing isn't just supported by the IList<T> interface, it's also supported by the IReadOnlyList<T> interface. Because IReadOnlyList<T> doesn't allow modification, it can be (and is) covariant just like IEnumerable<T> is.

So, just change the return type to IReadOnlyList<ISomeObject> and return the original list.

Of course, nothing prevents the caller from casting the result to List<SomeObject>, but the caller is supposed to have full access to that list anyway, so there is no security risk.

Upvotes: 3

Related Questions