List <T>或IList <T> [关闭]


495

谁能向我解释为什么我要在C#中使用IList over List?

相关问题:为什么认为暴露不好List<T>


1
互联网上搜索“到接口的代码而不是实现的代码”,您可以整日阅读..并找到几场激烈的战争。
granadaCoder

Answers:


446

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

如果您仅在内部使用它,则可能不太在意,List<T>可以使用。


19
我不了解(细微的)区别,是否有一些示例可以指出?
2008年

134
假设您最初使用List <T>,并且想要更改为使用专门的CaseInsensitiveList <T>,两者都实现了IList <T>。如果使用具体类型,则所有呼叫者都需要更新。如果公开为IList <T>,则不必更改调用方。
tvanfosson

46
啊。好。我明白了。选择合同的最低公分母。谢谢!
2008年

16
该原理是封装的一个示例,它是面向对象编程的三大支柱之一。这个想法是您向用户隐藏实现细节,而是为他们提供一个稳定的界面。这是为了减少对将来可能更改的细节的依赖。
杰森

14
还请注意T []:IList <T>,因此出于性能原因,您始终可以从从过程中返回List <T>到返回T [],并且如果过程被声明为返回IList <T>,也可以交换。 (或者ICollection <T>或IEnumerable <T>),则无需更改其他任何内容。
yfeldblum

322

不太受欢迎的答案是程序员喜欢假装他们的软件将在全球范围内重复使用,实际上,大多数项目将由少量人员维护,但是无论界面相关的声音如何,您都在欺骗你自己

建筑宇航员。您编写自己的IList将任何内容添加到.NET框架中已添加的内容的机会非常渺茫,以至于理论上的果冻只能用于“最佳实践”。

软件宇航员

显然,如果询问您在面试中使用哪一个,您会说出IList,微笑,并且都对自己如此聪明而感到满意。或面向公众的API,即IList。希望你明白我的意思。


65
我不同意你的讽刺回答。我并不完全不同意过度架构是一个真正的问题。但是,我认为尤其是在集合的情况下,接口确实发光。假设我有一个返回IEnumerable<string>的函数,在该函数内部,我可以将其List<string>用于内部后备存储来生成我的集合,但是我只希望调用者枚举其内容,而不是添加或删除。接受接口作为参数会传达类似的消息:“我需要一个字符串集合,不过不用担心,我不会更改它。”
joshperry 2010年

38
软件开发全部与翻译意图有关。如果您认为界面仅对构建超大的宏伟建筑有用,而在小商店中却没有位置,那么我希望面试中坐在您对面的那个人不是我。
joshperry

48
有人不得不说这是Arec ...即使结果是各种各样的学术模式都在追逐仇恨邮件。对于所有讨厌的人,当一个小应用程序加载了界面并单击“查找定义”时,我们会为此+1。我可以看到它会派上用场。
盖茨

6
如果可能的话,我会给你1000分。:D
塞缪尔

8
我说的是模式追逐的更广泛结果。通用接口的接口很好,为追求模式而增加的层不好。
2013年

186

接口是一个承诺(或合同)。

与承诺一样,越小越好


56
并不总是更好,只是更容易保持!:)
柯克·布罗德赫斯特

5
我不明白这一点。IList诺言也是如此。在这里,哪个更小更好的承诺?这是不合逻辑的。
nawfal

3
对于其他任何课程,我都会同意。但不是for List<T>,因为AddRange未在接口上定义。
杰西·德·威特

3
在这种特殊情况下,这实际上没有帮助。最好的诺言是信守诺言。IList<T>保证不能兑现。例如,Add如果IList<T>碰巧是只读的,有一种方法可以抛出该方法。List<T>另一方面,它始终是可写的,因此始终遵守其承诺。此外,自.NET 4.6起,我们现在拥有IReadOnlyCollection<T>IReadOnlyList<T>几乎总是比更好的适合性IList<T>。它们是协变的。避免IList<T>
ZunTzu

@ZunTzu我争辩说,有人假装不可变的收藏作为可变的收藏而故意违反了所提出的合同-这与合同本身无关,例如IList<T>。有人也可以实现IReadOnlyList<T>并使每个方法都抛出异常。这有点像鸭子式的对立面-您已经将其称为鸭子,但它不会像鸭子一样嘎嘎叫。尽管这是合同的一部分,即使编译器无法强制执行。我100%同意,IReadOnlyList<T>IReadOnlyCollection<T>应该用于只读访问,虽然。
谢尔比·奥德菲尔德

93

有人说“总是用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>没有。

Liskov替换原理(简化)指出,应该可以使用派生类型代替基本类型,而无需其他前提条件或后置条件。

感觉好像违反了Liskov替代原则。

 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[]什么?

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

在很多使用抽象而不是具体类型的论点中,单一责任原则/接口隔离原则具有吸引力,这取决于最窄的接口。在大多数情况下,如果您使用的是a,List<T>并且您认为可以使用更窄的界面-为什么不IEnumerable<T>呢?如果您不需要添加项目,则通常更合适。如果您需要添加到集合中,请使用具体类型,List<T>

对我IList<T>(和ICollection<T>)来说,这是.NET框架中最糟糕的部分。 IsReadOnly违反了最少惊讶的原则。Array不允许添加,插入或删除项目的类(例如)不应使用Add,Insert和Remove方法实现接口。(也可以看看 /software/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>,它指示是否可以从集合中添加或删除项目。但这只是使事情真正混乱,它并不能说明是否可以替换它们,对于Arrays(返回IsReadOnlys == true)来说可以。

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


5
很好,很好的答案。我想补充一点,即使IsReadOnlytrue对于IList,您仍然可以修改其当前成员!也许应该命名该属性IsFixedSize
xofz

(通过将Arrayas作为传递进行测试IList,所以这就是为什么我可以修改当前成员的原因)
xofz

1
@SamPearson在.NET 1中的IList上有一个IsFixedSize属性,该属性未包含在.NET 2.0的通用IList <T>中,该属性与IsReadOnly进行了合并,从而导致有关Arrays的意外行为。这里有一个关于细微差别的详细的让人头脑清醒的博客:enterprisecraftsmanship.com/2014/11/22/…–
完美主义者

7
很好的答案!此外,自.NET 4.6起,我们现在拥有IReadOnlyCollection<T>IReadOnlyList<T>几乎总是比IList<T>拥有更好的契合度,但没有危险的懒惰语义IEnumerable<T>。它们是协变的。避免IList<T>
ZunTzu

37

List<T>是的特定实现IList<T>,它是一个容器,可以T[]使用整数索引以与线性数组相同的方式进行寻址。当指定IList<T>方法的参数类型时,仅指定需要容器的某些功能。

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

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

此外,您可能需要考虑公开所需的最少功能。例如。如果你不需要更改列表的内容做,你应该考虑使用IEnumerable<T>,其IList<T>延伸。


关于您最近一次使用IEnumerable代替IList的声明。这并不总是建议,采取WPF例如,这将只是创建一个包装的IList对象,因此将有PERF的影响- msdn.microsoft.com/en-us/library/bb613546.aspx
哈迪斯

1
好的答案,性能保证是接口无法提供的。在某些代码中,这可能非常重要,使用具体的类可以传达您的意图和对特定类的需求。另一方面,一个接口说:“我只需要调用这套方法,就没有其他隐含的约定。”
joshperry

1
@joshperry如果界面的性能困扰您,那么您首先会在错误的平台上使用。对,这是微优化。
nawfal 2014年

@nawfal我没有评论接口的性能优缺点。我同意并评论了这样一个事实,当您选择公开一个具体的类作为参数时,您就能够传达出一种性能意图。接受LinkedList<T>vs接受List<T>vs IList<T>全部传达了所调用代码所要求的性能保证。
joshperry

28

我会稍微解决这个问题,而不是说明为什么要在具体实现上使用接口,而是要说明为什么要使用具体实现而不是接口。如果您无法证明其合理性,请使用该界面。


1
非常有趣的思考方式。这是编程时思考的一种好方法-谢谢。
花生

说得好!由于界面是更灵活的方法,因此您确实必须证明具体的实施方案可以为您带来什么好处。
科里之家

9
很棒的想法。我还是可以回答这个问题:我的原因叫AddRange()
PPC

4
我的原因叫做Add()。有时您想要一个列表,您肯定知道可以包含其他元素吗?看我的答案。
完美主义者

19

IList <T>是一个接口,因此您可以继承另一个类,并且仍然可以实现IList <T>,而继承List <T>可以阻止您这样做。

例如,如果有一个类A,而您的类B继承了它,那么您就不能使用List <T>

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

15

TDD和OOP的原理通常是编程到接口而不是实现。

在这种特定情况下,由于您本质上是在谈论一种语言构造,而不是自定义的一种,所以通常没关系,例如,说您发现List不支持您所需要的东西。如果您在应用程序的其余部分中使用了IList,则可以使用自己的自定义类扩展List,并且仍然可以在不进行重构的情况下将其传递出去。

这样做的成本很小,为什么以后不为自己省去头痛呢?这就是接口原理的全部内容。


3
如果您的ExtendedList <T>继承了List <T>,那么您仍然可以将其放入List <T>合同中。仅当您从sratch(或至少不继承List <T>)编写自己的IList <T>实现时,此参数才有效
PPC

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

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

IList <Bar>方式比List <Bar>方式宽松得多。


13

假设这些List vs IList问题(或答案)均未提及签名差异。(这就是为什么我在SO上搜索此问题的原因!)

因此,这里是List包含的方法,至少在.NET 4.5(大约2015年)中,这些方法在IList中找不到。

  • 添加范围
  • 只读
  • 二进制搜索
  • 容量
  • 全部转换
  • 存在
  • 找到所有
  • 查找索引
  • 最后查找
  • FindLastIndex
  • 每次
  • 获取范围
  • 插入范围
  • LastIndexOf
  • 移除所有
  • 移除范围
  • 逆转
  • 分类
  • 数组
  • 修剪过量
  • TrueForAll

1
不完全正确。ReverseToArray是扩展方法用于IEnumerable的<T>接口,它的IList <T>从导出。
Tarec 2015年

这是因为这些仅在List而不是在其他实现中才有意义IList
HankCa '18

12

在实现上使用接口的最重要情况是API的参数。如果您的API使用List参数,那么使用它的任何人都必须使用List。如果参数类型为IList,则调用者具有更大的自由度,并且可以使用您从未听说过的类,这些类甚至在编写代码时都不存在。


7

如果.NET 5.0替换System.Collections.Generic.List<T>System.Collection.Generics.LinearList<T>。.NET始终拥有该名称,List<T>但他们保证这IList<T>是合同。因此,恕我直言,我们(至少是我)不应该使用某人的名字(尽管在本例中为.NET),但以后会遇到麻烦。

在使用的情况下IList<T>,调用者总是要保证工作正常,实现者可以自由地将基础集合更改为的任何替代具体实现。IList


7

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

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

IList<T> MSDN链接

  1. 明确
  2. 包含
  3. 复制到
  4. GetEnumerator
  5. 指数
  6. 去掉
  7. RemoveAt

List<T> 实现了这九种方法(不包括扩展方法),此外,它还具有约41种公共方法,这权衡了您在应用程序中使用哪种方法。

List<T> MSDN链接


4

您可能会因为定义IList或ICollection将为接口的其他实现打开。

您可能需要一个IOrderRepository来定义IList或ICollection中的订单集合。然后,您可以使用不同类型的实现来提供订单列表,只要它们符合IList或ICollection定义的“规则”即可。



2

该接口确保您至少得到了您期望的方法 ; 了解接口的定义,即。所有继承该接口的类都可以实现的所有抽象方法。因此,如果某人用他从接口继承的一些附加功能之外的方法,用几种方法创建自己的巨大类,而这些方法对您没有用,那么最好使用对子类的引用(在这种情况下,接口)并为其分配具体的类对象。

另一个优点是您的代码不会对具体类进行任何更改,因为您只订阅具体类的方法中的几种,只要具体类从您所继承的接口继承,这些代码就将存在。使用。因此它对您来说是安全的,并为编写具体实现以更改其具体类或添加更多功能的编码人员提供了自由。


2

您可以从多个角度看待这个论点,包括一种纯粹的面向对象的方法,该方法表示针对接口而不是实现进行编程。考虑到这一点,使用IList遵循与传递和使用您从头定义的接口相同的原理。我也相信接口通常提供的可伸缩性和灵活性因素。如果需要扩展或更改实现IList <T>的类,则无需更改使用方代码;否则,无需更改。它知道IList Interface合同遵守什么。但是,在更改的类上使用具体实现和List <T>可能会导致调用代码也需要更改。这是因为遵守IList <T>的类可以保证某些行为,而使用List <T>的具体类型不能保证这种行为。

还具有在类上修改List <T>的默认实现的能力,例如,实现IList <T>的.Add,.Remove或任何其他IList方法为开发人员提供了很多灵活性和强大功能,否则将进行预定义按列表<T>


0

通常,一种好的方法是在面向公众的API中使用IList(在适当的情况下,并且需要列表语义),然后在内部使用List来实现API。这使您可以更改为IList的其他实现,而不会破坏使用您的类的代码。

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

请注意,如果您的API仅用于foreach循环等中,那么您可能需要考虑仅公开IEnumerable。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.