【问题标题】:Why generic IList<> does not inherit non-generic IList为什么泛型 IList<> 不继承非泛型 IList
【发布时间】:2013-01-11 14:47:45
【问题描述】:

IList&lt;T&gt; 不继承 IList,其中 IEnumerable&lt;out T&gt; 继承 IEnumerable

如果out 修饰符是唯一的原因,那么为什么大多数IList&lt;T&gt; 的实现(例如Collection&lt;T&gt;List&lt;T&gt;)都实现了IList 接口。

所以任何人都可以说好的,如果该陈述对于IList&lt;T&gt; 的所有实现都是正确的,那么在必要时直接将其转换为IList。但问题是虽然IList&lt;T&gt; 不继承IList 所以不能保证每个IList&lt;T&gt; 对象都是IList

此外,使用IList&lt;object&gt; 显然不是解决方案,因为没有out 修饰符,泛型不能分配给继承较少的类;并且在这里创建 List 的新实例不是解决方案,因为有人可能希望将 IList&lt;T&gt; 的实际引用作为 IList 指针;并使用 List&lt;T&gt; 而不是 IList&lt;T&gt; 实际上是一种糟糕的编程习惯,并不能满足所有目的。

如果 .NET 想要提供灵活性,IList&lt;T&gt; 的每个实现都不应该有非泛型实现的合同(即IList),那么为什么他们不保留另一个实现泛型和非泛型的接口版本,并没有建议所有想要为通用和非遗传项目收缩的具体类都应该通过该接口收缩。

ICollection&lt;T&gt; 转换为ICollectionIDictionary&lt;TKey, TValue&gt; 转换为IDictionary 也会出现同样的问题。

【问题讨论】:

  • @JonSkeet 我认为他的文章有误。 Ienumerable 是协变的而不是逆变的。 .在文章中搜索这个确切的短语:“事实证明,唯一可行的通用接口是 IEnumerable,因为只有 IEnumerable 是逆变的:”
  • 不要担心你在数学上的困难。我能向你保证,我的依然更好。 阿尔伯特爱因斯坦,为您的指导:D
  • @RoyiNamir:是的,我认为你是对的 - 但文章的其余部分仍然合适。
  • @Nafeez Abrar:您需要解决的实际问题是什么?

标签: c# .net generics library-design


【解决方案1】:

如您所见,IList&lt;T&gt; 中的T 不是协变的。根据经验:任何可以修改其状态的类都不能是协变的。原因是此类类通常具有将T 作为其参数之一的类型的方法,例如void Add(T element)input 位置中不允许使用协变类型参数。

除了其他原因外,还添加了泛型以提供类型安全。例如,您不能将Elephant 添加到Apple 的列表中。如果ICollection&lt;T&gt; 扩展ICollection,那么您可以调用((ICollection)myApples).Add(someElephant) 而不会出现编译时错误,因为ICollection 有一个方法void Add(object obj),它似乎允许您添加any对象到列表中,而实际上您只能添加 T 的对象。因此,ICollection&lt;T&gt; 不扩展 ICollectionIList&lt;T&gt; 不扩展 IList

Anders Hejlsberg,C# 的创造者之一,explains it like this

理想情况下,所有泛型集合接口(例如ICollection&lt;T&gt;IList&lt;T&gt;)都将从它们的非泛型对应物继承,这样泛型接口实例既可以用于泛型代码也可以用于非泛型代码。

事实证明,唯一可行的通用接口是IEnumerable&lt;T&gt;,因为只有IEnumerable&lt;T&gt; 是反变体[sic1]:在IEnumerable&lt;T&gt; 中,类型参数T 仅用于“输出”位置(返回值),而不用于“输入”位置(参数)。 ICollection&lt;T&gt;IList&lt;T&gt; 在输入和输出位置都使用 T,因此这些接口是不变的。

1) IEnumerable&lt;T&gt;co-变体


从 .Net 4.5 开始,就有了 IReadOnlyCollection&lt;out T&gt;IReadOnlyList&lt;out T&gt; 协变接口。但是IList&lt;T&gt;ICollection&lt;T&gt; 以及许多列表和集合类都没有实现或扩展它们。坦率地说,我发现它们不是很有用,因为它们只定义了 Countthis[int index]


如果我可以从头开始重新设计 .Net 4.5,我会将列表接口拆分为只读协变接口IList&lt;out T&gt;,其中包括ContainsIndexOf,以及一个可变不变接口IMutableList&lt;T&gt; .然后你可以将IList&lt;Apple&gt; 转换为IList&lt;object&gt;。我在这里实现了这个:

M42 Collections - 协变集合、列表和数组。

【讨论】:

  • IList&lt;T&gt; 的任何实现都可以合法地实现IList,即让IList.IsReadOnly 返回true,即使IList&lt;T&gt;.IsReadOnly 为假。注意String[] 实现了IList&lt;String&gt;IList&lt;Object&gt;;前者的IsReadOnly 属性将返回false,但后者的IsReadOnly 属性将返回true,因此实现多个“可能可写”接口并可由某些但不是全部可写的集合的想法几乎是不可能的未知。
  • 您的答案是正确的,但第一个链接/引用的描述奇怪地不正确。具体来说,IEnumerable 是Covariant,而不是Contra-variant。请参阅this answer 以供参考。我建议把它说出来以避免更多的混乱。
【解决方案2】:

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

public interface IReadOnlyList<out T>

its documentation

List&lt;YourClass&gt;Collection&lt;YourClass&gt;YourClass[] 等通常的集合类型确实实现了 IReadOnlyList&lt;YourClass&gt;,并且由于协方差也可以用作 IReadOnlyList&lt;SomeBaseClass&gt; 并最终用作 IReadOnlyList&lt;object&gt;

如您所料,您将无法通过IReadOnlyList&lt;&gt; 参考修改您的列表。

使用这个新界面,您也许可以同时避免非泛型IList。但是你仍然会遇到IReadOnlyList&lt;T&gt; 不是IList&lt;T&gt; 的基本接口的问题。

【讨论】:

  • 实际上,从 .NET 4.0 开始就支持泛型中的协变和逆变。 .NET 4.5 中只添加了像IReadOnlyList&lt;out T&gt; 这样的只读接口。
【解决方案3】:

创建一个接口MyIList&lt;T&gt;,让它继承自IList&lt;T&gt;IList

public interface MyIList<T> : IList<T>, IList
{ }

现在创建一个类MySimpleList并让它实现MyIList&lt;T&gt;

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 在问题的 cmets 中已经提到了 article from Brad

【讨论】:

  • 许多IList 方法都需要if(!(x is T)) throw ... 检查。使实现IList 更加烦人。
  • @CodesInChaos:是的,如果你要实现这个类,这是会遇到的问题之一。
  • @Oliver 实际问题是将System.Collection.Generic.IList&lt;T&gt; 实例转换为System.Collection.IList 指针。所以写另外一个接口并不能很好的解决这个问题。要使用此解决方案,必须使用MyIList&lt;T&gt; 并为每个继承IList&lt;T&gt;IList 的具体类编写单独的包装器,如class ListWrapper : List, MyIList {} 和构造函数(如有必要)。但是,如果 .NET List&lt;&gt;(以及实际上同时继承 IList&lt;&gt;IList 的其他类) 继承像 MyIList 这样的接口会更容易。
  • 我不想说,你应该使用自己的界面。它简单地说明了为什么 IList 不从 IList 继承。如果您尝试自己这样做(如我的示例中),那么您会发现遇到了很多问题,这就是为什么微软也没有这样做并单独留下界面的原因。
【解决方案4】:

已经给出了很好的答案。 不过关于 IList 的通知:

MSDN IList Remarks: “IList 实现分为三类:只读、固定大小和可变大小。(...)。有关此接口的通用版本,请参阅 System.Collections.Generic.IList&lt;T&gt;。”

这有点误导,因为在通用方面,我们将 IList&lt;T&gt; 设置为 variable-size,并将 IReadOnlyList&lt;T&gt; 从 4.5 开始设置为只读,但 AFAIK 没有固定大小的通用列表。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-05
    • 2010-09-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多