从List <t>有效地返回IList <interface>(避免从List <t>转换为List <i>)

时间:2018-05-25 09:47:53

标签: c# interface casting

我有以下代码:

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有一个公共属性Objects,它返回类类型的List。客户知道类类型可以使用它来做任何他们想做的事情。仅了解ISomeObject的客户只能使用Objects属性来获取IList<ISomeObject>。因为不允许将List<SomeObject>投射到IList<ISomeObject>(由于apple and banana issue),我需要一种方法来转换它。默认方式,使用Cast.ToList()可以工作,但是每次评估属性时都会创建一个新List,这可能很昂贵。将ISomeObject.Objects更改为返回IEnumerable<ISomeObject>还有另一个缺点,即客户端无法再使用索引(这在我的用例中非常相关)。当在IEnumerable上使用时,重复使用Linq的ElementAt()调用是昂贵的。

有没有人知道如何避免这两个问题? (当然,在任何地方都知道SomeObject不是一种选择。

3 个答案:

答案 0 :(得分:4)

您可以/应该实现类似于ReadOnlyCollection<T>的类来充当代理。考虑到它是只读的,它可能是“协变的”(不是语言方面,但逻辑上,这意味着它可以代理TDest的子类/接口的TSource然后{ {1}}用于所有写入方法。

像这样(代码未经测试):

throw NotSupportedException()

使用它像:

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

答案 1 :(得分:3)

  

更改ISomeObject.Objects以返回IEnumerable<ISomeObject>还有另一个缺点,即客户端无法再使用索引(这在我的用例中非常相关)。

索引不仅仅由IList<T>界面支持,IReadOnlyList<T>界面也支持索引。由于IReadOnlyList<T>不允许修改,因此它可以(并且是)协变,就像IEnumerable<T>一样。

因此,只需将返回类型更改为IReadOnlyList<ISomeObject>并返回原始列表。

当然,没有什么可以阻止调用者将结果转换为List<SomeObject>,但调用者应该完全访问该列表,因此不存在安全风险。

答案 2 :(得分:0)

您可能希望尝试封装List<SomeObject>,使其成为实施细节,然后返回IReadOnlyList<SomeObject>。然后,由于SomeObject差异,ISomeObjectIReadOnlyList广告在界面实施中也是不必要的 - 您将能够将Objects作为IReadOnlyList<ISomeObject>返回。

然后只需添加一些操作即可将基础列表(如AddRemove)变更为容器类型(如果需要)。

另外我应该提到界面不太适合限制 - 邪恶的消费者可以轻松地将你的ISomeObject投射到SomeObject并做他想做的一切,可能你应该重新考虑你的设计。你最好坚持使用不变性和封装这样的东西来提供可用的api。明确地使用可变构建器,然后在合理的情况下使用不可变类。