如何从List<T>高效返回IList<Interface>?避免强制转换与重复拷贝
先还原你的场景代码:
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. } } }
你面临的核心矛盾是:List<SomeObject>无法直接转换为IList<ISomeObject>(因为IList<T>是不变类型),复制列表开销太高,改成IEnumerable又丢失了关键的索引访问能力。下面是几个能解决这个问题的方案:
方案1:实现轻量的集合包装类(最推荐)
我们可以写一个通用的包装类,把List<SomeObject>包装成IList<ISomeObject>——这样既不需要复制原列表,还能完整支持索引访问,而且原列表的任何修改都会实时同步到包装后的集合里。关键是我们可以在SomeObject中缓存这个包装实例,避免每次访问都创建新对象。
先写通用包装类:
public class CovariantListWrapper<TImpl, TInterface> : IList<TInterface> where TImpl : class, TInterface { private readonly IList<TImpl> _innerList; public CovariantListWrapper(IList<TImpl> innerList) { _innerList = innerList ?? throw new ArgumentNullException(nameof(innerList)); } public TInterface this[int index] { get => _innerList[index]; set { if (value is not TImpl impl) throw new ArgumentException($"Value must be of type {typeof(TImpl).Name}", nameof(value)); _innerList[index] = impl; } } public int Count => _innerList.Count; public bool IsReadOnly => _innerList.IsReadOnly; public void Add(TInterface item) { if (item is not TImpl impl) throw new ArgumentException($"Item must be of type {typeof(TImpl).Name}", nameof(item)); _innerList.Add(impl); } public void Clear() => _innerList.Clear(); public bool Contains(TInterface item) => item is TImpl impl && _innerList.Contains(impl); public void CopyTo(TInterface[] array, int arrayIndex) { for (int i = 0; i < _innerList.Count; i++) { array[arrayIndex + i] = _innerList[i]; } } public IEnumerator<TInterface> GetEnumerator() { foreach (var item in _innerList) yield return item; } public int IndexOf(TInterface item) => item is TImpl impl ? _innerList.IndexOf(impl) : -1; public void Insert(int index, TInterface item) { if (item is not TImpl impl) throw new ArgumentException($"Item must be of type {typeof(TImpl).Name}", nameof(item)); _innerList.Insert(index, impl); } public bool Remove(TInterface item) => item is TImpl impl && _innerList.Remove(impl); public void RemoveAt(int index) => _innerList.RemoveAt(index); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
然后修改你的SomeObject类,添加缓存逻辑:
public class SomeObject : ISomeObject { private IList<ISomeObject> _interfaceObjectsWrapper; public SomeObject() { Objects = new List<SomeObject>(); } public List<SomeObject> Objects { get; set; } IList<ISomeObject> ISomeObject.Objects { get { // 缓存包装实例,避免重复创建 return _interfaceObjectsWrapper ??= new CovariantListWrapper<SomeObject, ISomeObject>(Objects); } } }
这个方案的优势:
- 完全避免列表复制,性能开销几乎可以忽略
- 保留
IList<T>的全部功能(索引访问、增删改等) - 原列表和包装集合实时同步,修改任意一方都会影响另一方
- 包装类是通用的,可以复用在其他类似的协变场景中
方案2:改用IReadOnlyList<ISomeObject>(只读场景适用)
如果你的接口客户端只需要读取集合(不需要修改),那可以把接口的Objects属性改成返回IReadOnlyList<ISomeObject>——因为IReadOnlyList<T>是协变类型,List<SomeObject>可以直接隐式转换为IReadOnlyList<ISomeObject>,完全不需要额外处理:
public interface ISomeObject { IReadOnlyList<ISomeObject> Objects { get; } } public class SomeObject : ISomeObject { public SomeObject() { Objects = new List<SomeObject>(); } public List<SomeObject> Objects { get; set; } // 直接返回原列表,隐式转换为IReadOnlyList<ISomeObject> IReadOnlyList<ISomeObject> ISomeObject.Objects => Objects; }
这个方案最简单,但前提是接口客户端不需要修改集合。如果需要修改集合的话,这个方案就不适用了。
方案3:类中同时保留强类型和接口类型属性(折中方案)
如果你不想写包装类,也可以调整类的结构,让Objects属性直接实现接口类型,同时给知晓类的客户端提供一个强类型访问器:
public interface ISomeObject { IList<ISomeObject> Objects { get; } } public class SomeObject : ISomeObject { public SomeObject() { Objects = new List<ISomeObject>(); } // 直接实现接口的属性 public IList<ISomeObject> Objects { get; set; } // 给知晓类的客户端提供强类型访问方式 public IEnumerable<SomeObject> StrongTypedObjects => Objects.OfType<SomeObject>(); // 如果确定集合中只有SomeObject实例,可以用Cast<SomeObject>(),OfType更安全 }
不过这个方案的问题是,如果客户端需要List<SomeObject>而不是IEnumerable<SomeObject>,每次调用ToList()还是会复制列表,只是把复制的代价转移到了类的客户端这边。如果客户端频繁访问这个属性,性能还是会受影响,所以这个方案不如第一个方案理想。
内容的提问来源于stack exchange,提问作者PMF

