List< T>或IList< T>

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

相关问题:为什么暴露List<T>被认为是坏的

233455 次浏览

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

如果您只是在内部使用它,您可能不会太在意,使用List<T>可能就可以了。

IList< T>是一个接口,所以你可以继承另一个类,仍然实现IList<T>同时继承List<T>阻止你这样做。

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

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

因为定义IList或ICollection将为接口的其他实现打开空间。

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

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

在这个特定的情况下,因为你实际上是在谈论一个语言结构,而不是一个自定义的,这通常并不重要,但比如说,你发现List不支持你需要的东西。如果你在应用的其余部分使用了IList,你可以用你自己的自定义类扩展List,并且仍然能够在不重构的情况下传递它。

这样做的成本是最小的,为什么不省去以后的麻烦呢?这就是界面原则。

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

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

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

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

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

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

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

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

界面是一个承诺(或合同)。

因为它总是与承诺- 越小越好

我将把这个问题稍微转一下,与其证明为什么应该使用接口而不是具体实现,不如尝试证明为什么应该使用具体实现而不是接口。如果你不能证明这一点,那就使用界面。

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

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

软件宇航员

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

# EYZ0;注意接口的定义。由继承接口的任何类实现的所有抽象方法。因此,如果有人创建了一个巨大的类,除了他从接口继承的一些附加功能之外,还使用了几个方法,而这些方法对你没有用处,最好使用一个子类的引用(在这种情况下是接口),并将具体的类对象赋值给它。

额外的好处是,你的代码是安全的,不受任何对具体类的更改,因为你只订阅了具体类的几个方法,而这些方法是那些只要具体类继承了你正在使用的接口就会存在的方法。所以这对你来说是安全的,对编写具体实现的编码器来说是自由的,他可以改变或添加更多的功能到他的具体类中。

您可以从几个角度来看待这个论点,包括一个纯面向对象方法,即针对接口而不是实现进行编程。有了这样的想法,使用IList与传递和使用从头定义的接口遵循相同的原则。一般来说,我也相信接口所提供的可伸缩性和灵活性。如果一个类实现IList<T>需要扩展或更改时,消费代码不必更改;它知道IList接口契约遵循什么。然而,使用具体的实现和List<T>在更改的类上,可能会导致调用代码也需要更改。这是因为一个类坚持IList<T>使用List<T>来保证具体类型无法保证的特定行为。

还可以做一些事情,比如修改List<T>实现IList<T>例如,.Add, .Remove或任何其他IList方法为开发人员提供了很多的灵活性和功能,否则由List<T>

根据另一个帖子的建议,IList<>几乎总是更可取的,但是当使用WCF DataContractSerializer运行IList<>进行多个序列化/反序列化循环时,请注意.NET 3.5 sp 1有一个bug

现在有一个SP来修复这个错误:KB 971030

如果。net 5.0将System.Collections.Generic.List<T>替换为System.Collection.Generics.LinearList<T>, . net始终拥有List<T>的名称,但他们保证IList<T>是一个契约。所以恕我直言,我们(至少我)不应该使用别人的名字(尽管在这种情况下是。net),之后会遇到麻烦。

在使用IList<T>的情况下,调用者总是被保证工作,而实现者可以自由地将底层集合更改为IList的任何替代具体实现

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

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

# EYZ0 # EYZ1

  1. 添加
  2. 清晰的
  3. 包含
  4. CopyTo
  5. GetEnumerator
  6. IndexOf
  7. 插入
  8. 删除
  9. RemoveAt

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

# EYZ0 # EYZ1

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

下面是List中包含的在IList中找不到的方法,至少在。net 4.5(大约2015年)

  • AddRange
  • AsReadOnly
  • BinarySearch
  • 能力
  • ConvertAll
  • 存在
  • 找到
  • FindAll
  • FindIndex
  • FindLast
  • FindLastIndex
  • ForEach
  • GetRange
  • InsertRange
  • LastIndexOf
  • RemoveAll
  • RemoveRange
  • 反向
  • 排序
  • ToArray
  • TrimExcess
  • TrueForAll
有人说“总是用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的更多信息,请参见ICollection<T>的定义。IsReadOnly

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

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

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