有人能向我解释一下为什么我想在c#中使用IList而不是List吗?

相关问题:为什么公开List<T>被认为是不好的


当前回答

有人说“总是使用IList<T>而不是List<T>”。 他们想让你把你的方法签名从void Foo(List<T> input)改成void Foo(IList<T> input)。

这些人错了。

事实比这更微妙。如果返回一个IList<T>作为库的公共接口的一部分,则为将来创建自定义列表留下了一些有趣的选项。你可能永远不需要这个选项,但它是一个参数。我认为这是返回接口而不是具体类型的全部理由。值得一提的是,在这种情况下,它有一个严重的缺陷。

作为一个小小的反驳,您可能会发现每个调用者都需要一个List<T>,并且调用代码中充斥着.ToList()

但更重要的是,如果你接受一个IList作为参数,你最好小心,因为IList<T>和List<T>的行为不一样。尽管名称相似,尽管共享接口,但它们并不公开相同的契约。

假设你有这样的方法:

public Foo(List<int> a)
{
    a.Add(someNumber);
}

一位乐于助人的同事“重构”该方法以接受IList<int>。

你的代码现在坏了,因为int[]实现了IList<int>,但是是固定的大小。ICollection<T> (IList<T>的基础)的契约要求在尝试从集合中添加或删除项之前使用它检查IsReadOnly标志的代码。List<T>的契约没有。

利斯科夫替换原则(简化)指出派生类型应该能够用来代替基类型,没有额外的先决条件或后置条件。

这似乎打破了利斯科夫替换原则。

 int[] array = new[] {1, 2, 3};
 IList<int> ilist = array;

 ilist.Add(4); // throws System.NotSupportedException
 ilist.Insert(0, 0); // throws System.NotSupportedException
 ilist.Remove(3); // throws System.NotSupportedException
 ilist.RemoveAt(0); // throws System.NotSupportedException

但事实并非如此。答案是这个例子使用了IList<T>/ICollection<T>错误。如果你使用ICollection<T>,你需要检查IsReadOnly标志。

if (!ilist.IsReadOnly)
{
   ilist.Add(4);
   ilist.Insert(0, 0); 
   ilist.Remove(3);
   ilist.RemoveAt(0);
}
else
{
   // what were you planning to do if you were given a read only list anyway?
}

如果有人传递给你一个数组或列表,如果你每次都检查标志并有一个回退,你的代码将正常工作…但真的;谁会这么做?你事先不知道你的方法是否需要一个可以接受额外成员的列表吗?难道你没有在方法签名中指定吗?如果你被传递一个像int[]这样的只读列表,你到底要做什么?

您可以将List<T>替换为正确使用IList<T>/ICollection<T>的代码。您不能保证可以将IList<T>/ICollection<T>替换为使用List<T>的代码。

在使用抽象而不是具体类型的许多参数中,使用单一责任原则/接口隔离原则(依赖于尽可能窄的接口)是有吸引力的。在大多数情况下,如果您正在使用List<T>,并且您认为可以使用更窄的接口,为什么不使用IEnumerable<T>呢?如果你不需要添加项目,这通常是一个更好的选择。如果需要添加到集合,请使用具体类型List<T>。

对我来说,IList<T>(和ICollection<T>)是。net框架中最糟糕的部分。IsReadOnly违反最小意外原则。从不允许添加、插入或删除项的类(如Array)不应该实现具有添加、插入和删除方法的接口。(参见https://softwareengineering.stackexchange.com/questions/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties)

IList<T>是否适合您的组织?如果有同事要求您将方法签名改为使用IList<T>而不是List<T>,请询问他们如何将元素添加到IList<T>中。如果他们不知道IsReadOnly(大多数人都不知道),那么就不要使用IList<T>。永远。


注意IsReadOnly标志来自ICollection<T>,表示是否可以从集合中添加或删除项;但它并没有指出它们是否可以被替换,这在数组(返回isreadonly == true)的情况下是可以的。

有关IsReadOnly的更多信息,请参见msdn definition of ICollection<T>。IsReadOnly

其他回答

public void Foo(IList<Bar> list)
{
     // Do Something with the list here.
}

在这种情况下,您可以传入任何实现了IList<Bar>接口的类。如果使用List<Bar>代替,则只能传入一个List<Bar>实例。

IList<Bar>方式比List<Bar>方式耦合更松。

所有的概念基本上都在上面关于为什么使用接口而不是具体实现的回答中说明了。

IList<T> defines those methods (not including extension methods)

IList<T> MSDN link

添加 清晰的 包含 CopyTo GetEnumerator IndexOf 插入 删除 RemoveAt

List<T>实现了这9个方法(不包括扩展方法),除此之外,它还有大约41个公共方法,这将影响您在应用程序中使用哪个方法。

List<T> MSDN link

通常,一个好的方法是在面向公共的API中使用IList(在适当的情况下,并且需要列表语义),然后在内部使用list来实现API。这允许您在不破坏使用您的类的代码的情况下更改到不同的IList实现。

类名List可能在下一个。net框架中改变,但接口永远不会改变,因为接口是契约。

注意,如果你的API只会在foreach循环中使用,那么你可能会考虑只暴露IEnumerable。

List<T>是IList<T>的特定实现,它是一个容器,可以像线性数组T[]一样使用整数索引寻址。当您指定IList<T>作为方法参数的类型时,您只是指定了您需要容器的某些功能。

例如,接口规范并不强制使用特定的数据结构。List<T>的实现在访问、删除和添加元素方面与线性数组具有相同的性能。但是,您可以想象一个由链表支持的实现,其中在末尾添加元素更便宜(常量时间),但随机访问要昂贵得多。(注意。net LinkedList<T>没有实现IList<T>。)

这个例子还告诉您,在某些情况下,您可能需要在参数列表中指定实现,而不是接口:在这个例子中,当您需要特定的访问性能特征时。这通常是容器的特定实现所保证的(List<T>文档:“它使用一个数组实现IList<T>泛型接口,该数组的大小根据需要动态增加。”)。

此外,您可能需要考虑公开最少需要的功能。为例。如果你不需要改变列表的内容,你可能应该考虑使用IEnumerable<T>,它是IList<T>扩展的。

不太流行的答案是,程序员喜欢假装他们的软件在世界各地都可以重用,而事实上,大多数项目将由少数人维护,无论界面相关的声音有多好,你都在欺骗自己。

架构宇航员。你编写自己的IList,为。net框架中已经存在的IList添加任何东西的机会是如此渺茫,以至于它只是理论上的“最佳实践”。

很明显,如果你被问到在面试中使用什么,你会说IList,微笑,两个人看起来都为自己的聪明感到高兴。或者对于一个面向公众的API, IList。希望你明白我的意思。