为什么通用的IList<>不继承非通用的IList

16

IList<T>不继承IList,而IEnumerable<out T>继承IEnumerable

如果只有out修饰符是唯一的原因,那么为什么大多数IList<T>的实现(例如Collection<T>List<T>)实现了IList接口呢?

因此,任何人都可以说:如果这个语句对于IList<T>的所有实现都成立,那么在必要时直接将其转换为IList。但问题在于,虽然IList<T>没有继承IList,所以并不能保证每个IList<T>对象都是IList

此外,使用IList<object>显然不是解决方案,因为没有out修饰符,泛型不能被分配到一个较低的继承类;在这里创建List的新实例也不是解决方案,因为有人可能希望将IList<T>的实际引用作为IList指针使用;而使用List<T>来代替IList<T>实际上是一个糟糕的编程实践,不能满足所有目的。

如果.NET想要提供灵活性,即不是每个IList<T>实现都必须具有非泛型实现(即IList)的契约,则为什么他们没有保留另一个接口,该接口同时实现了泛型和非泛型版本,并建议所有希望对泛型和非泛型项进行契约的具体类通过该接口进行契约。

ICollection<T>强制转换为ICollectionIDictionary<TKey,TValue>强制转换为IDictionary也会出现同样的问题。


14
请查看http://blogs.msdn.com/b/brada/archive/2005/01/18/355755.aspx。 - Jon Skeet
4
@JonSkeet 我认为他在文章中犯了一个错误。IEnumerable是协变的,而不是逆变的。请在文章中搜索以下确切短语:“事实证明,唯一可能实现这一点的通用接口是IEnumerable<T>,因为只有IEnumerable<T>是逆变的:” - Royi Namir
不要担心你在数学上遇到的困难。我可以向你保证,我的困难更大。——阿尔伯特·爱因斯坦,为您指引 :D - spajce
@RoyiNamir:是的,我认为你是对的 - 但文章的其余部分仍然适用。 - Jon Skeet
@CodesInChaos:当涉及到泛型时,反射变得非常棘手。仅仅为了在集合上调用“Add”方法就显得过于繁琐。但你是对的,这是一个非常罕见的情况。无论如何,我喜欢拥有一个漂亮的接口,不受编译时类型信息的限制。问题不在于非泛型已经过时,而在于Microsoft没有成功地创建有用和一致的集合接口。 - Stefan Steinegger
显示剩余5条评论
4个回答

9
正如您所指出的,IList<T>中的T不是协变的。作为一个经验法则:任何能够修改其状态的类都不能是协变的。原因在于这样的类通常有将T作为其中一个参数类型的方法,例如void Add(T element)。而协变类型参数是不允许在输入位置使用的。

泛型被添加进来,除了其他原因外,主要是为了提供类型安全。例如,你不能将一个 Elephant 添加到 Apple 的列表中。如果 ICollection<T> 扩展了 ICollection,那么你可以调用 ((ICollection)myApples).Add(someElephant) 而不会在编译时出错,因为 ICollection 有一个方法 void Add(object obj),看起来允许你添加任何对象到列表中,但实际上你只能添加 T 类型的对象。因此,ICollection<T> 不扩展 ICollectionIList<T> 也不扩展 IList

C# 的创造者之一 Anders Hejlsberg 这样解释

理想情况下,所有通用集合接口(例如ICollection<T>IList<T>)都应该继承它们的非通用对应接口,以便在通用和非通用代码中都可以使用通用接口实例。事实证明,唯一可以这样做的通用接口是IEnumerable<T>,因为只有IEnumerable<T>是协变的:在IEnumerable<T>中,类型参数T仅用于“输出”位置(返回值),而不用于“输入”位置(参数)。ICollection<T>IList<T>在输入和输出位置都使用T,因此这些接口是不变的。
自 .Net 4.5 起,引入了IReadOnlyCollection<out T>IReadOnlyList<out T>协变接口。但是IList<T>ICollection<T>以及许多列表和集合类并没有实现或扩展它们。坦白地说,我认为它们并不是非常有用,因为它们只定义了Countthis[int index]
如果我能从头开始重新设计.NET 4.5,我会将列表接口拆分为只读协变接口 IList<out T>,其中包括ContainsIndexOf,以及可变的不变式接口 IMutableList<T>。然后,您可以将 IList<Apple> 强制转换为 IList<object>。我在这里实现了这个想法:

M42 Collections - Covariant collections, lists and arrays.


1
任何 IList<T> 的实现都可以通过将 IList.IsReadOnly 返回为 true 来合法地实现 IList,即使 IList<T>.IsReadOnlyfalse。请注意,String[] 同时实现了 IList<String>IList<Object>;前者的 IsReadOnly 属性将返回 false,但后者的 IsReadOnly 属性将返回 true,因此实现多个“可能可写”接口并且某些接口可写而另一些接口不可写的集合的概念并不陌生。 - supercat
2
你的答案是正确的,但第一个链接/引用的描述有些不正确。具体来说,IEnumerable是Covariant,而不是Contra-variant。参考这个答案。 我建议将此指出以避免更多的混淆。 - TheRubberDuck

3

请注意,自2012年以来,在.NET 4.5及更高版本中,存在一个协变的(out修饰符)接口,

public interface IReadOnlyList<out T>

请参阅其文档。通常的集合类型,如List<YourClass>Collection<YourClass>YourClass[]都实现了IReadOnlyList<YourClass>接口,并且由于协变性也可以用作IReadOnlyList<SomeBaseClass>和最终的IReadOnlyList<object>。正如您所猜测的那样,您将无法通过IReadOnlyList<>引用修改列表。有了这个新接口,您可能完全可以避免非泛型IList。但是,您仍然会遇到IReadOnlyList<T>不是IList<T>的基接口的问题。

实际上,自 .NET 4.0 起就支持泛型中的协变和逆变。只有像 IReadOnlyList<out T> 这样的只读接口是在 .NET 4.5 中添加的。 - O. R. Mapper

1
创建一个接口MyIList<T>,并让它继承自IList<T>IList
public interface MyIList<T> : IList<T>, IList
{ }

现在创建一个名为 MySimpleList 的类,并让它实现 MyIList<T> 接口:
public class MySimpleList<T> : MyIList<T>
{
    public int Count
    {
        get { throw new NotImplementedException(); }
    }

    public bool IsFixedSize
    {
        get { throw new NotImplementedException(); }
    }

    public bool IsReadOnly
    {
        get { throw new NotImplementedException(); }
    }

    public bool IsSynchronized
    {
        get { throw new NotImplementedException(); }
    }

    public object SyncRoot
    {
        get { throw new NotImplementedException(); }
    }

    object IList.this[int index]
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public T this[int index]
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public void Add(T item)
    {
        throw new NotImplementedException();
    }

    public int Add(object value)
    {
        throw new NotImplementedException();
    }

    public void Clear()
    {
        throw new NotImplementedException();
    }

    public bool Contains(T item)
    {
        throw new NotImplementedException();
    }

    public bool Contains(object value)
    {
        throw new NotImplementedException();
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }

    public void CopyTo(Array array, int index)
    {
        throw new NotImplementedException();
    }

    public IEnumerator<T> GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    public int IndexOf(T item)
    {
        throw new NotImplementedException();
    }

    public int IndexOf(object value)
    {
        throw new NotImplementedException();
    }

    public void Insert(int index, T item)
    {
        throw new NotImplementedException();
    }

    public void Insert(int index, object value)
    {
        throw new NotImplementedException();
    }

    public bool Remove(T item)
    {
        throw new NotImplementedException();
    }

    public void Remove(object value)
    {
        throw new NotImplementedException();
    }

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

现在您可以很容易地看到,您需要双重实现一堆方法。一个是针对类型T的,一个是针对对象的。在正常情况下,您希望避免这种情况。这是协变和逆变的问题。
关于IList和IList之间的具体问题,最好的解释可以在Jon在问题评论中提到的Brad的文章中找到。

3
很多IList方法需要进行if(!(x is T)) throw ...检查,这使得实现IList更加烦人。 - CodesInChaos
@CodesInChaos:是的,如果你要实现这个类,那就会遇到其中的一个问题。 - Oliver
1
@Oliver 实际问题是将 System.Collection.Generic.IList<T> 实例转换为 System.Collection.IList 指针。因此编写另一个接口不能成为解决此问题的好方法。 要使用此解决方案,必须使用 MyIList<T> 并为每个继承 IList<T>IList 的具体类编写单独的包装器,例如 class ListWrapper : List, MyIList {},并在必要时编写构造函数。 但是,如果 .NET List<>(以及其他实际上同时继承 IList<>IList 的类)继承像 MyIList 这样的接口,则会更容易些。 - Nafeez Abrar
我不想说,你应该使用自己的接口。它只是说明了为什么IList<T>没有继承自IList。如果你尝试像我例子中那样这样做,你会发现遇到很多问题,这也是微软没有这样做并将接口分开的原因。 - Oliver

0

已经给出了好的答案。 关于IList的注意事项:

MSDN IList Remarks: “IList实现分为三类:只读、固定大小和可变大小。(...). 对于此接口的泛型版本,请参见 System.Collections.Generic.IList<T>。”

这有点误导人,因为在泛型方面,我们有IList<T>作为可变大小, 而自4.5以来,IReadOnlyList<T>是只读的,但据我所知,没有固定大小的泛型列表。


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