协变性和IList

56

我希望有一个协变的集合,可以通过索引检索其项。IEnumerable是我所知道的唯一协变的.NET集合,但它不支持索引。

具体来说,我想做到这一点:

List<Dog> dogs = new List<Dog>();

IEnumerable<Animal> animals = dogs;
IList<Animal> animalList = dogs; // This line does not compile

我现在明白为什么这是个问题了。List实现了有Add方法的ICollection接口。将其向上转型为Animal类型的IList,会允许后续代码添加任何类型的动物,而这在“真正”的List<Dog>集合中是不被允许的。

那么有人知道支持索引查找并且也是协变的集合吗?我不想自己创建。


7
除了IEnumerable之外,在.NET中缺乏只读集合接口,这使得这几乎是不可能的,但我想这是一个常见的用例,也许有人已经想出了可行的解决方案。 备注:将原句翻译成通俗易懂的中文时,保持了原意和表达方式,没有添加解释或其他内容。 - Konrad Rudolph
你可以使用IEnumerable<>和ElementAt()一起使用,尽管语法不会那么漂亮。 - Felix Ungman
4个回答

66
更新:从.NET 4.5开始,有IReadOnlyList<out T>IReadOnlyCollection<out T>,它们都是协变的;后者基本上是IEnumerable<out T>加上Count;前者添加了T this[int index] {get;}。还应该注意的是,从.NET 4.0开始,IEnumerable<out T>也是协变的。 List<T>ReadOnlyCollection<T>(通过List<T>.AsReadOnly())都实现了这两个接口。

只有当它只有一个get索引器时,它才能是协变的。

public T this[int index] { get; }

但是所有的主要集合都有{get;set;},这使得它变得尴尬。我不知道是否有足够的东西可以解决这个问题,但是你可以包装它,即编写一个扩展方法:

var covariant = list.AsCovariant();

这是一个围绕着 IList<T> 的包装器,只公开了 IEnumerable<T>get 索引器...? 只需要几分钟的工作...

public static class Covariance
{
    public static IIndexedEnumerable<T> AsCovariant<T>(this IList<T> tail)
    {
        return new CovariantList<T>(tail);
    }
    private class CovariantList<T> : IIndexedEnumerable<T>
    {
        private readonly IList<T> tail;
        public CovariantList(IList<T> tail)
        {
            this.tail = tail;
        }
        public T this[int index] { get { return tail[index]; } }
        public IEnumerator<T> GetEnumerator() { return tail.GetEnumerator();}
        IEnumerator IEnumerable.GetEnumerator() { return tail.GetEnumerator(); }
        public int Count { get { return tail.Count; } }
    }
}
public interface IIndexedEnumerable<out T> : IEnumerable<T>
{
    T this[int index] { get; }
    int Count { get; }
}

谢谢。这似乎是最好的前进方式。 - Brian M
2
有时候我希望微软能够找到一种方法,让IList继承自子接口IReadableByIndex(你称之为IIndexedEnumerable)、IWritableByIndex和iAppendable,以便允许有用的协变性和逆变性。不幸的是,到目前为止,只读或只写属性无法由读写属性实现。如果可以做到这一点,协变/逆变子接口的所有成员都将自然地由任何有效的IList实现来实现,因此可以添加子接口而不会破坏现有代码。 - supercat
鉴于这样的事情是不可能的,你的方法也许是最好的前进方式。我猜在协变性出现之前,全能的IList可能还不错,但是必须有一个可读写的索引器而不是一个只读索引器和一个写索引器的必要性使得对于大多数集合来说,协变/逆变无用。顺便说一句,我想知道为什么具有只读属性和只写属性但没有可读写索引器的东西不能被很好地读取和写入?为什么编译器不能选择实际起作用的最佳属性呢? - supercat
非常棒的答案。只是想指出,尽管IEnumerable<out T>从.NET 4.0开始变成了协变,但是截至今天的.NET Standard 2.1,IList<T>仍然保持不变,因此微软强制我们坚持使用IReadOnlyList<out T>。 - Lesair Valmont
@Lesair 这不是疏忽; 这是因为 IList<T> 不能 协变 (或逆变), 因为 API 表面(允许你放入和取出值)与协变的要求不兼容。所以这不是 Microsoft 强制的,而是 IList<T> 的本质。 - Marc Gravell

9

这是我写的一个类来解决这个场景:

public class CovariantIListAdapter<TBase, TDerived> : IList<TBase>
    where TDerived : TBase
{
    private IList<TDerived> source;

    public CovariantIListAdapter(IList<TDerived> source)
    {
        this.source = source;
    }

    public IEnumerator<TBase> GetEnumerator()
    {
        foreach (var item in source)
            yield return item;
    }

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

    public void Add(TBase item)
    {
        source.Add((TDerived) item);
    }

    public void Clear()
    {
        source.Clear();
    }

    public bool Contains(TBase item)
    {
        return source.Contains((TDerived) item);
    }

    public void CopyTo(TBase[] array, int arrayIndex)
    {
        foreach (var item in source)
            array[arrayIndex++] = item;
    }

    public bool Remove(TBase item)
    {
        return source.Remove((TDerived) item);
    }

    public int Count
    {
        get { return source.Count; }
    }

    public bool IsReadOnly
    {
        get { return source.IsReadOnly; }
    }

    public int IndexOf(TBase item)
    {
        return source.IndexOf((TDerived) item);
    }

    public void Insert(int index, TBase item)
    {
        source.Insert(index, (TDerived) item);
    }

    public void RemoveAt(int index)
    {
        source.RemoveAt(index);
    }

    public TBase this[int index]
    {
        get { return source[index]; }
        set { source[index] = (TDerived) value; }
    }
}

现在你可以这样编写代码:
List<Dog> dogs = new List<Dog>();
dogs.Add(new Dog { Name = "Spot", MaximumBarkDecibals = 110 });

IEnumerable<Animal> animals = dogs;
IList<Animal> animalList = new CovariantIListAdapter<Animal, Dog>(dogs);

animalList.Add(new Dog { Name = "Fluffy", MaximumBarkDecibals = 120 });

变更在两个列表中都可以看到,因为实际上只有一个列表。适配器类只是通过传递调用,根据需要将项目强制转换为所需的 IList<TBase> 接口。
显然,如果除了 Dogs 之外添加任何东西到 animalList,它将抛出异常,但这满足了我的需求。

1
你不需要为此创建一个新的适配器。只需创建一个新的列表,就像这样:new List<Animal>(dogs),一切都会正常工作。虽然你可以这样做,但这并不能解决逆变问题。 - Shoaib Shakeel
这是多态性,即使你添加了一个对象,它与Animal也不同。这个链接https://msdn.microsoft.com/en-us/library/ee207183.aspx讲述了协变性和逆变性。 - natnael88
你可以按照同样的模式编写一个ContravarianceIListAdapter。建议:编写扩展方法,隐藏这些适配器,以便您可以编写dogs.AsList<Animal>()。 - Felix Keil

5

从技术上讲,有一个数组集合。它的差异性有些破碎,但它可以完成你所要求的功能。

IList<Animal> animals;
List<Dog> dogs = new List<Dog>();
animals = dogs.ToArray();

如果您试图在数组中放置一只Tiger,那么在运行时,它肯定会引起惊人的错误。


4
自 .NET Framework 4.5 起,存在一个协变的接口 IReadOnlyList,它与 Mark Gravell 的回答中的 IIndexedEnumerable 接口本质上是相同的。
IReadOnlyList 的实现如下:
  /// <summary>
  /// Represents a read-only collection of elements that can be accessed by index.
  /// </summary>
  /// <typeparam name="T">The type of elements in the read-only list. This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics.</typeparam>
    public interface IReadOnlyList<out T> : IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
      {
        /// <summary>
        /// Gets the element at the specified index in the read-only list.
        /// </summary>
        /// 
        /// <returns>
        /// The element at the specified index in the read-only list.
        /// </returns>
        /// <param name="index">The zero-based index of the element to get. </param>
        T this[int index] { get; }
      }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接