List<T>或IList<T>

559

9
这个问题并不基于观点。 List<T>, IList<T>, 以及 T[] 在不同的场景下更为合适,它们之间的差异通常是相当客观的。就像这个问题,两个问题有很多共同之处,但可以说不完全重复。但无论哪种情况,这都不是基于观点的问题。可能发生的情况是,关闭者只看了这个问题的标题,而没有阅读问题本身。问题的正文是客观的。 - Panzercrisis
18个回答

490
如果你通过一个库来暴露你的类,让别人使用,通常你应该通过接口而不是具体实现来暴露它。这将有助于以后决定更改类的实现以使用不同的具体类时。在那种情况下,库的用户不需要更新他们的代码,因为接口没有改变。
如果只是内部使用,可能就不那么关心了,使用 List<T> 可能是可以的。

20
我不理解(微妙的)区别,你能给我举个例子吗? - StingyJack
153
假设您最初使用了List<T>,现在想要改用专门的CaseInsensitiveList<T>,两者都实现了IList<T>接口。如果使用具体类型,则需要更新所有调用者。如果将其公开为IList<T>接口,则不需要更改调用者。 - tvanfosson
58
啊,好的。我知道了。为合同选择最低公约数。谢谢! - StingyJack
19
这个原则是面向对象编程的三大支柱之一——封装的一个实例。其思想是将实现细节对用户进行隐藏,取而代之地提供一个稳定的接口,以减少未来可能改变的细节所带来的依赖。 - jason
14
请注意,T[]: IList<T>。因此出于性能考虑,您可以随时从过程返回List<T>并改为返回T[],如果该过程声明为返回IList<T>(或可能是ICollection<T>或IEnumerable<T>),则无需进行其他更改。 - yfeldblum
显示剩余7条评论

358

程序员喜欢假装他们的软件将被全世界重复使用,实际上大多数项目都只由少数人维护,无论接口相关的说辞有多好听,你都在欺骗自己。

架构宇航员。你编写自己的IList,并为.NET框架中已经存在的列表添加新功能的机会是如此渺茫,以至于这只是“最佳实践”的理论糖果。

软件宇航员

显然,如果在面试中被问到使用哪个,你应该选择IList,微笑着,相互庆祝自己的聪明才智。或者对于公共API,使用IList。希望你明白我的意思。


71
我不同意你的嘲讽性回答。我不完全反对过度架构是一个真正的问题的观点。然而,我认为特别是在集合的情况下,接口确实非常出色。假设我有一个返回IEnumerable<string>的函数,在函数内部,我可能会使用List<string>作为内部备份存储来生成我的集合,但我只想让调用者枚举它的内容,而不是添加或删除。接受一个接口作为参数传递了类似的信息:“我需要一个字符串的集合,不过不要担心,我不会改变它。” - joshperry
46
软件开发的核心在于将意图转化为实现。如果你认为接口仅用于构建过大、浮夸的架构,在小型商店中没有用处,那么我希望坐在你面前面试的人不是我。 - joshperry
56
有人必须说出这句话:Arec(译者注:文中可能有错别字,原文未给出完整信息,此处翻译按原文来)……就算你因此收到各种学术模式追逐的仇恨邮件也要说。对于我们这些讨厌一个小应用程序被界面所淹没,而点击“查找定义”时却跳转到问题来源之外的地方的人来说,点个赞。我能用“架构宇航员”这个词吗?我看到它会派上用场。 - Gats
8
如果可能的话,我会给您1000个积分作为此答案的奖励。 :D - Samuel
10
我在谈论追踪模式的更广泛结果。对于常见接口的接口是好的,但仅为了追踪模式而添加额外的层是不好的。 - Gats
显示剩余9条评论

195

接口是一种承诺(或者契约)。

就像承诺一样,越小越好


65
并不总是更好,只是更容易维持! :) - Kirk Broadhurst
7
我不理解这个。所以IList是承诺。哪一个是更小、更好的承诺?这不合逻辑。 - nawfal
4
对于其他类,我同意。但是对于 List<T> 不同意,因为接口上没有定义 AddRange 方法。 - Jesse de Wit
5
在这种情况下,这并不是真正有帮助的。最好的承诺是实现的承诺。IList<T>做了它无法实现的承诺。例如,如果 IList<T> 是只读的,则可能会抛出异常的 Add 方法。而 List<T> 是可写的,因此始终实现它的承诺。另外,自 .NET 4.6 以来,我们现在拥有 IReadOnlyCollection<T>IReadOnlyList<T>,它们几乎总是比 IList<T> 更适合使用。它们也是协变的。避免使用 IList<T> - ZunTzu
1
@Shelby Oldfield 我同意将不可变集合作为 IList 传递是一个明显的错误,自从 .NET 4.6 以来(并且很可能会被编译器捕获)。但是还有更隐匿的情况,比如将 C# 数组作为 IList 传递。我不确定每个人都知道数组实现了 IList,这意味着不能假定支持 Add。 - ZunTzu
显示剩余2条评论

126

有些人说“总是使用 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)不应实现具有 Add、Insert 和 Remove 方法的接口。(另见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> ,表示是否可以向集合添加或删除项;但是,仅仅为了混淆事情,它不表示是否可以替换它们,而对于返回 IsReadOnlys = true 的数组来说,可以替换它们。
了解更多关于 IsReadOnly 的信息,请参见 ICollection<T>.IsReadOnly 的 MSDN 定义

10
非常好的回答。我想补充一点,即使对于一个IListIsReadOnlytrue,你仍然可以修改它当前的成员!也许这个属性应该被命名为IsFixedSize - xofz
这段代码已经测试过将一个“Array”作为“IList”传递,并修改了当前成员。 - xofz
1
@SamPearson,在.NET 1中的IList上有一个IsFixedSize属性,但在.NET 2.0的泛型IList<T>中不包括它,而是与IsReadOnly合并,导致数组周围出现令人惊讶的行为。这里有一篇详细的令人头痛的博客,介绍了微妙的差异:http://enterprisecraftsmanship.com/2014/11/22/read-only-collections-and-lsp/ - perfectionist
12
非常棒的答案!另外,自从.NET 4.6版本以来,我们现在有了IReadOnlyCollection<T>IReadOnlyList<T>,它们几乎总是比IList<T>更合适,但不具有IEnumerable<T>的潜在危险惰性语义,并且它们是协变的。避免使用IList<T>! - ZunTzu

38
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对象,因此会影响性能 - http://msdn.microsoft.com/en-us/library/bb613546.aspx - HAdes
1
很好的回答,性能保证是接口无法提供的。在某些代码中,这可能非常重要,使用具体类可以传达您的意图,您需要那个特定类。另一方面,接口表示“我只需要调用这组方法,没有其他约定暗示”。 - joshperry
1
如果接口的性能让你困扰,那么你首先就选择了错误的平台。在我看来,这是微观优化。 - nawfal
@nawfal 我没有在评论接口的性能优缺点。我是在赞同并评论当您选择将具体类公开为参数时,您能够传达性能意图的事实。使用 LinkedList<T> 而不是 List<T>IList<T> 传达了代码调用所需的某些性能保证。 - joshperry

29

我会稍作修改,把问题转化一下,不要解释为什么你应该使用接口而不是具体实现,而是试图证明为什么你应该使用具体实现而不是接口。如果你无法证明,就使用接口。


1
思考方式很有趣,也是编程时的一种好方法 - 谢谢。 - Peanut
说得好!由于接口是更灵活的方法,你确实应该证明具体实现给你带来了什么好处。 - Cory House
9
很棒的思维方式。不过我能回答它:我的理由叫做AddRange()。 - PPC
5
我的理由被称为Add()。 有时候你想要一个列表,你确定可以添加额外的元素,对吧?请看我的答案。 - perfectionist

20

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

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

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

16
public void Foo(IList<Bar> list)
{
     // Do Something with the list here.
}
在这种情况下,您可以传递实现了IList<Bar>接口的任何类。如果使用List<Bar>,则只能传递List<Bar>实例。
使用IList<Bar>方式比List<Bar>方式更松散耦合。

15

TDD和OOP的一个原则是编程时面向接口而不是实现。

在这种特定情况下,因为你基本上谈论的是一种语言结构,而不是自定义结构,通常并不重要。但是,假设例如你发现List没有支持你需要的某些内容。如果你在应用程序的其他地方使用了IList,你可以使用自己的自定义类扩展List,并仍然能够传递它,而无需进行重构。

这样做的成本很小,为什么不在以后避免麻烦呢?这就是接口原则的全部意义。


4
如果你的 ExtendedList<T> 继承了 List<T>,那么你仍然可以将其适配到 List<T> 的协议中。这个说法只有在你自己编写实现 IList<T> 的代码(至少不继承 List<T>)时才成立。 - PPC

14

令人惊讶的是,所有这些List vs IList的问题(或答案)都没有提到签名差异。 (这就是为什么我在SO上搜索这个问题!)

所以以下是List包含但IList中没有的方法,至少截至.NET 4.5(大约在2015年):

  • AddRange(添加范围)
  • AsReadOnly(转只读)
  • BinarySearch(二进制搜索)
  • Capacity(容量)
  • ConvertAll(转换全部)
  • Exists(存在)
  • Find(查找)
  • FindAll(查找全部)
  • FindIndex(查找索引)
  • FindLast(查找最后一个)
  • FindLastIndex(查找最后一个索引)
  • ForEach(遍历)
  • GetRange(获取范围)
  • InsertRange(插入范围)
  • LastIndexOf(最后一个索引位置)
  • RemoveAll(移除全部)
  • RemoveRange(移除范围)
  • Reverse(反转)
  • Sort(排序)
  • ToArray(转为数组)
  • TrimExcess(去除多余空间)
  • TrueForAll(全部符合条件)

1
并不完全正确。ReverseToArray是IEnumerable<T>接口的扩展方法,而IList<T>继承自该接口。 - Tarec
这是因为这些只在List中才有意义,而不适用于IList的其他实现。 - HankCa

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