关于C#:List or IList

List<T> or IList<T>

有人能给我解释一下为什么我要在C中使用IList over list吗?

相关问题:为什么曝光EDOCX1[0]被认为是不好的?


如果您通过其他人将使用的库来公开您的类,那么通常您希望通过接口而不是具体的实现来公开它。如果您决定稍后更改类的实现以使用不同的具体类,这将有所帮助。在这种情况下,库的用户不需要更新他们的代码,因为界面不会改变。

如果您只是在内部使用它,您可能不太在意,使用List可能没问题。


不那么流行的答案是程序员喜欢假装他们的软件将在世界各地被重新使用,当事实发生时,大多数项目将由少数人维护,不管与界面相关的声音多么美妙,你都在欺骗自己。

建筑宇航员。您编写自己的IList,在.NET框架中添加任何内容的机会是如此遥远,以至于它是为"最佳实践"保留的理论上的软糖。

Software astronauts

很明显,如果有人问你在面试中使用哪一种语言,你会说"我最爱你",微笑着,两人都很高兴自己这么聪明。或者对于面向公众的API,ilist。希望你明白我的意思。


接口是承诺(或合同)。

就像总是有承诺一样-越小越好。


有人说"总是用IList代替List"。他们希望您将方法签名从void Foo(List input)更改为void Foo(IList input)

这些人错了。

比这更微妙。如果您要将IList作为公共接口的一部分返回到您的库,那么您可以为自己留一些有趣的选项,以便将来创建自定义列表。你可能永远不需要这个选择,但这是一个论点。我认为这是返回接口而不是具体类型的整个参数。值得一提的是,在这种情况下,它有一个严重的缺陷。

作为一个小小的反驳,你可能会发现每一个调用者无论如何都需要一个List,而且调用代码中充斥着.ToList()

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

假设您有这种方法:

1
2
3
4
public Foo(List<int> a)
{
    a.Add(someNumber);
}

一个有帮助的同事"重构"接受IList的方法。

您的代码现在被破坏了,因为int[]实现了IList,但大小是固定的。ICollection合同(IList的基础)要求在试图从集合中添加或删除项之前使用该合同检查IsReadOnly标志的代码。List的合同没有。

Liskov替换原则(简化)规定,派生类型应该能够代替基类型,而不需要附加的前置条件或后置条件。

这似乎打破了里斯科夫替代原理。

1
2
3
4
5
6
7
 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/icolection错误。如果使用ICollection,则需要检查IsReadOnly标志。

1
2
3
4
5
6
7
8
9
10
11
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替换为正确使用IList/ICollection的代码。您不能保证可以将IList/ICollection替换为使用List的代码。

在许多使用抽象而不是具体类型的论据中,对单一责任原则/接口分离原则的吸引力在于尽可能窄的接口。在大多数情况下,如果您使用的是List,并且您认为可以使用更窄的接口来代替-为什么不使用IEnumerable?如果您不需要添加项目,这通常是一个更好的适合。如果需要添加到集合中,请使用具体类型List

对我来说,IListICollection是.NET框架中最糟糕的部分。IsReadOnly违反了最小惊喜原则。不允许添加、插入或删除项的类(如Array)不应实现带有添加、插入和删除方法的接口。(另请参见https://softwarengineering.stackexchange.com/questions/306105/implementation-an-interface-when-you-dont-need-one-of-the-properties)

IList是否适合您的组织?如果同事要求您更改方法签名以使用IList而不是List,请询问他们如何向IList添加元素。如果他们不了解IsReadOnly(大多数人不知道),那么就不要使用IList。曾经。

请注意,isreadOnly标志来自ICollection,它指示是否可以添加项或从集合中移除项;但为了真正混淆某些内容,它不指示是否可以替换它们,在数组(返回isreadOnlys==true)的情况下,可以替换这些项。

有关isreadonly的详细信息,请参见msdn definition of icolection.isreadonly。


ListIList的具体实现,它是一个容器,可以使用整数索引以与线性数组T[]相同的方式寻址。当您指定IList作为方法参数的类型时,您只指定需要容器的某些功能。

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

此示例还告诉您,在参数列表中可能存在需要指定实现而不是接口的情况:在本示例中,每当需要特定的访问性能特征时。这通常保证容器的特定实现(List文档:"它使用一个数组实现IList通用接口,该数组的大小根据需要动态增加。")。

另外,您可能想考虑公开您所需要的最少的功能。例如。如果您不需要更改列表的内容,您可能应该考虑使用IEnumerableIList扩展了它。


我将把问题转过来一点,而不是解释为什么您应该在具体实现上使用接口,而是尝试解释为什么您要使用具体实现而不是接口。如果您不能证明它是正确的,那么使用接口。


ilist是一个接口,因此您可以继承另一个类,并且在继承list时仍然实现ilist

例如,如果有一个类A,而您的类B继承了它,则不能使用list

1
class A : B, IList<T> { ... }

TDD和OOP的一个原则通常是编程到接口,而不是实现。

在这个特定的例子中,因为您本质上是在谈论一个语言结构,而不是一个定制的结构,所以通常情况下这并不重要,但是举个例子来说,您发现列表不支持您需要的东西。如果在应用程序的其余部分中使用了ilist,那么可以使用自己的自定义类扩展列表,并且仍然能够在不重构的情况下传递列表。

这样做的成本是最低的,为什么以后不省去自己的头痛呢?这就是接口原则的意义所在。


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

在这种情况下,可以传入实现IList接口的任何类。如果使用list代替,则只能传入list实例。

与list方法相比,ilist方法的耦合更松散。


支持这些列表和IList问题(或答案)中没有提到签名差异。(这就是为什么我在上面搜索这个问题!)

下面是列表中所包含的方法,这些方法在IList中找不到,至少在.NET 4.5中是如此(大约在2015年)。

  • 阿德兰奇
  • 只读的
  • 二进制搜索
  • 容量
  • 收敛的
  • 存在
  • 发现
  • 芬德尔
  • 发现指数
  • 芬德拉特
  • 查找索引
  • 前额
  • 格特兰格
  • 插入法
  • 连续索引
  • 移除所有
  • 远距
  • 反向
  • 排序
  • 托托
  • 三次过量
  • 特雷福尔


在实现上使用接口最重要的情况是在API的参数中。如果您的API接受一个列表参数,那么任何使用它的人都必须使用列表。如果参数类型是ilist,那么调用者有更多的自由,可以使用您从未听说过的类,这些类在编写代码时甚至可能不存在。


如果.NET 5.0将System.Collections.Generic.List替换为System.Collection.Generics.LinearList呢?.net始终拥有List的名称,但它们保证IList是一个合同。所以我们(至少我)不应该使用某人的名字(尽管在本例中是.NET),以后会遇到麻烦。

在使用IList的情况下,调用者总是被保证可以工作,并且实现者可以自由地将底层集合更改为IList的任何其他具体实现。


关于为什么在具体实现上使用接口,上面的大多数答案基本上都说明了所有概念。

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

IListmsdn链路

  • 添加
  • 清除
  • 包含
  • 方法
  • 方法
  • 索引
  • 插入
  • 去除
  • 去掉
  • List实现了这九种方法(不包括扩展方法),除此之外,它还有大约41种公共方法,这在您考虑在应用程序中使用哪一种方法时很重要。

    Listmsdn链路


    因为定义一个IList或ICollection将为接口的其他实现打开大门。

    您可能希望拥有一个IORDerRepository,它定义IList或ICollection中的订单集合。然后,您可以使用不同类型的实现来提供订单列表,只要它们符合由IList或ICollection定义的"规则"。


    根据其他海报的建议,IList<>几乎总是可取的,但是请注意,当使用WCF DataContractSerializer通过一个以上的序列化/反序列化循环运行IList<>时,.NET 3.5 SP 1中存在一个错误。

    现在有一个SP可以修复此错误:kb 971030


    接口确保您至少获得了预期的方法;了解接口的定义,即,继承接口的任何类都要实现那里的所有抽象方法。因此,如果有人用几个方法自己创建了一个巨大的类,除了他从接口继承的方法之外,还有一些其他功能,而这些方法对您没有用处,那么最好使用对子类的引用(在本例中是接口)并将具体的类对象分配给它。

    另外一个好处是,您的代码可以安全地避免对具体类的任何更改,因为您只订阅了具体类的几个方法,只要具体类继承自您使用的接口,这些方法就可以存在。因此,对于编写具体实现以更改或向其具体类添加更多功能的编码人员来说,这是安全的,也是自由的。


    您可以从多个角度来看待这个论点,包括纯OO方法中的一个,它表示要针对接口而不是实现进行编程。有了这个想法,使用IList遵循相同的原则,即传递和使用从头定义的接口。我还相信一般情况下由接口提供的可伸缩性和灵活性因素。如果需要扩展或更改实现ilist的类,则消费代码不必更改;它知道ilist接口契约遵循的是什么。但是,对更改的类使用具体的实现和list可能会导致调用代码也需要更改。这是因为依附于ilist的类可以保证某些行为,而使用list的具体类型并不能保证这些行为。

    还可以在实现ilist的类上修改list的默认实现,比如说.add、.remove或任何其他ilist方法,这给了开发人员很大的灵活性和能力,否则由list


    通常,一个好的方法是在面向公共的API中使用IList(在适当的时候,需要列表语义),然后在内部列出以实现API。&这允许您在不破坏使用类的代码的情况下更改为IList的其他实现。

    类名列表可以在下一个.NET框架中更改,但接口永远不会更改,因为接口是契约。

    注意,如果您的API只在foreach循环等中使用,那么您可能会考虑只公开IEnumerable。