何时使用IList和何时使用List?

211

我知道IList是接口类型,而List是具体类型,但我仍然不知道何时应该使用它们中的哪一个。现在我做的是,如果我不需要Sort或FindAll方法,我就使用接口。我是对的吗?有没有更好的方法来决定何时使用接口或具体类型?


2
如果有人还在疑惑,我认为最好的答案在这里:https://dev59.com/h3RC5IYBdhLWcg3wJNcN - Crismogram
12个回答

208

我遵循两个规则:

  • 接受最基本的类型即可
  • 返回用户所需的最丰富类型

因此,在编写接受集合的函数或方法时,不要使用List,而是使用IList<T>、ICollection<T>或IEnumerable<T>。即使对于异构列表,通用接口仍将起作用,因为System.Object也可以是T。如果您决定在更深处使用Stack或其他数据结构,则这样做会使您省去麻烦。如果函数中需要做的只是循环遍历,则真正需要请求的只是IEnumerable<T>。

另一方面,当从函数中返回对象时,您希望给用户提供尽可能丰富的操作集,而不必进行转换。因此,在这种情况下,如果内部是List<T>,则将其作为List<T>的副本返回。


55
您不应该将输入/输出类型视为不同的对象。输入和输出类型应该都是最基本的类型(最好是接口),以支持客户端的需求。封装依赖于尽可能少地向客户端揭示类的实现细节。如果您返回一个具体的列表(List),则不能更改为其他更好的类型,否则就需要强制所有客户端重新编译或更新。 - Ash
17
我不同意这两个规则……我会使用最原始的类型,特别是在返回IList(最好是IEnumerable)时,并且你应该在函数内部使用List。当需要“添加”或“排序”时,再使用Collection,如果需要更多,则使用List。因此,我的硬性规定是:始终从IEnumerable开始,如果需要更多,则进行扩展…… - ethem
3
为了方便起见,“两个规则”有一个名称:健壮性原则(又称波斯特尔法则) - easoncxz
8
我非常强烈地反对第二点,特别是当它涉及服务/ API 边界时。返回可修改的集合可能会给人一种集合是“实时”的印象,并且调用像 Add()Remove() 这样的方法可能会产生超出集合之外的影响。对于数据检索方法,通常最好返回只读接口,例如 IEnumerable。您的使用者可以按需将其投射到更丰富的类型中。 - STW
1
唉,IList<T> 是唯一允许快速索引访问 [] 的集合接口。通常我的要求之一是返回这样一个索引集合。我的另一个要求是能够更改实际的集合(列表、数组等),而不需要 API 更改,这意味着返回类型应该是一个接口。这些要求带我们来到了 IList<T>。不幸的是,这也授予了 Add(),以及其他方法(甚至对于数组也会 抛出 异常!)。因此,集合接口并不是理想的选择。或者,通过调用 resultList.AsReadOnly() 返回一个 ReadOnlyCollection<T> - Timo
显示剩余3条评论

64

根据FxCop检查,Microsoft的指南建议在公共API中避免使用List<T>,而优先使用IList<T>。

顺带一提,现在我几乎总是将一维数组声明为IList<T>,这意味着我可以始终使用IList<T>.Count属性而不是Array.Length。例如:

public interface IMyApi
{
    IList<int> GetReadOnlyValues();
}

public class MyApiImplementation : IMyApi
{
    public IList<int> GetReadOnlyValues()
    {
        List<int> myList = new List<int>();
        ... populate list
        return myList.AsReadOnly();
    }
}
public class MyMockApiImplementationForUnitTests : IMyApi
{
    public IList<int> GetReadOnlyValues()
    {
        IList<int> testValues = new int[] { 1, 2, 3 };
        return testValues;
    }
}

4
我最喜欢这个解释/例子! - JonH

39

IEnumerable
您应该尽可能使用最不具体的类型来满足您的需求。
IEnumerableIList不太具体。
当您想遍历集合中的项目时,可以使用IEnumerable

IList
IList实现了IEnumerable
当您需要通过索引访问集合、添加和删除元素等操作时,应使用IList

List
List实现了IList


8
非常好的回答,我把它标记为有用的。然而,我想补充一点,对于大多数开发者来说,绝大部分时间内,程序大小和性能方面微小的差异并不值得担心:如果不确定,就使用List即可。 - Graham Laight

32

有一件重要的事情,人们似乎总是忽略:

你可以将普通数组传递给接受 IList<T> 参数的方法,然后调用 IList.Add() 会收到运行时异常:

Unhandled Exception: System.NotSupportedException: Collection was of a fixed size.

例如,考虑以下代码:

private void test(IList<int> list)
{
    list.Add(1);
}

如果您按照以下方式调用,则会收到运行时异常:

int[] array = new int[0];
test(array);

这是因为使用普通数组与 IList<T> 违反了 Liskov 替换原则。

出于这个原因,如果你要调用 IList<T>.Add(),你可能会考虑要求一个 List<T> 而不是一个 IList<T>


2
这对于每个接口都是显然的真理。如果你想坚持你的论点,那么你可以主张根本不使用任何接口,因为它的某些实现可能会抛出异常。另一方面,如果你考虑到OP提供的建议,更喜欢使用List<T>而不是IList<T>,你也应该了解为什么推荐使用IList<T>。(例如https://blogs.msdn.microsoft.com/kcwalina/2005/09/26/why-we-dont-recommend-using-listt-in-public-apis/) - Micha Wiedenmann
4
我的回答适用于调用 IList<T>.Add() 方法的情况。我并不是说你不应该使用 IList<T> - 我只是指出了可能会遇到的问题。如果可以的话,我通常更喜欢使用IEnumerable<T>IReadOnlyList<T>IReadOnlyCollection<T>而不是IList<T> - Matthew Watson

24
我同意Lee的建议,应该传递参数但不返回值。
如果你指定方法返回一个接口,那么你可以在不影响调用方法的情况下随时更改具体实现。我曾经认为永远不需要从List<T>更改,但后来需要使用自定义列表库提供的额外功能进行更改。因为我只返回了IList<T>,所以使用库的人都不需要更改他们的代码。
当然,这仅适用于外部可见的方法(即公共方法)。我个人甚至在内部代码中使用接口,但是如果您进行破坏性更改,您可以自己更改所有代码,因此这并非绝对必要。

9

尽可能使用最低基础类型,这样你的接口实现者或方法使用者就有机会在幕后使用他们喜欢的任何内容。

对于集合,应尽可能使用IEnumerable。这提供了最大的灵活性,但并不总是适用。


1
最好始终接受可能的最低基本类型。返回则是另一回事。选择哪些选项可能有用。所以你认为你的客户可能想要使用索引访问?防止他们对已经是列表的返回的IEnumerable<T>进行ToList()操作,而是返回一个IList<T>。现在,客户可以轻松地从您提供的内容中受益。 - Timo

5

如果你只在单个方法内(甚至只在某些情况下的单个类或程序集内)工作,而且没有人外部会看到你在做什么,那么使用List的完整性。但是如果你要与外部代码交互,例如从方法返回列表时,那么你只需要声明接口而不必将自己绑定到特定实现,特别是如果你无法控制谁编译你的代码之后。如果你最初使用了具体类型,并决定更改为另一个类型,即使它使用相同的接口,你也会破坏其他人的代码,除非你最初使用了接口或抽象基础类型。


4

在大多数情况下,建议您使用最常用的通用类型,即IList或者更好的IEnumerable接口,以便稍后可以方便地切换实现方式。

然而,在.NET 2.0中,有一件令人烦恼的事情 - IList没有一个Sort()方法。您可以使用提供的适配器代替:

ArrayList.Adapter(list).Sort()

2
我认为这种事情没有硬性规定,但通常我会遵循使用最轻量级的方式直到绝对必要的指导方针。
例如,假设您有一个“Person”类和一个“Group”类。一个“Group”实例有许多人,因此在这里使用List是有意义的。当我在“Group”中声明列表对象时,我将使用“IList<Person>”并将其实例化为“List”。
public class Group {
  private IList<Person> people;

  public Group() {
    this.people = new List<Person>();
  }
}

而且,如果您甚至不需要 IList 中的所有内容,您也可以始终使用 IEnumerable。随着现代编译器和处理器的发展,我认为实际上没有速度差异,因此这更多只是一种风格问题。


5
为什么一开始不直接把它做成List呢?我还是不明白为什么你要把它做成IList,然后在构造函数里再转换成List<>。 - chobo2
我同意,如果你明确地创建了一个List<T>对象,那么你就失去了接口的优势。 - The_Butcher
这似乎仍然没有涵盖使用 IList 而不是 List 的原因。 - Bonez024

2

只有在需要时,例如,如果您的列表被转换为除List以外的IList实现,才应使用接口。例如,当您使用NHibernate时,它会将ILists转换为NHibernate bag对象来检索数据。

如果List是您将永远使用的某个集合的唯一实现,请随意将其声明为具体的List实现。


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