数组 vs List<T>: 什么时候使用哪个?

749
MyClass[] array;
List<MyClass> list;

什么情况下应该选择其中一个?为什么?


13
数组已经相当过时了,正如在这里的一个热门讨论中所看到的那样。同时,在这里指出,以及我们的主持人在博客中中也提到了这一点。 - gimel
11
如果我没记错,List<> 的内部结构是一个数组。当内部数组被填满时,它会简单地将内容复制到一个大小为原来两倍的数组中(或者是当前大小的某个常数倍)。参见http://en.wikipedia.org/wiki/Dynamic_array。 - Ykok
Ykok:你说的似乎没错,我在这里找到了List<>的源代码 - Carol
42
主张数组已过时可能有些大胆。 - awdz9nld
如果我需要处理大量的数据/对象,我会使用数组,因为我发现数组比列表更快地迭代。如果我需要添加或删除集合中的项目,我倾向于只使用列表。 - Phil
16个回答

732

实际上很少需要使用数组,任何时候想要添加/删除数据都应该使用 List<T> ,因为调整数组大小是昂贵的。如果您知道数据长度固定,并且您想要针对某些非常特定的原因进行微观优化(经过基准测试),那么数组可能有用。

List<T> 比数组提供了更多功能(尽管 LINQ 差不多),几乎总是正确的选择。当然,除了 params 参数之外。 ;-p

作为对比 - List<T> 是一维的;而你可以有诸如 int[,] string[,,] 等矩形(等)数组 - 但在对象模型中还有其他建模此类数据的方式(如果需要)。

另请参见:

话虽如此,我在我的protobuf-net项目中大量使用数组;完全是为了性能:

  • 它进行了许多位移操作,因此编码时 byte[] 几乎是必需的;
  • 我使用本地滚动的 byte[] 缓冲区,在发送到底层流之前填充该缓冲区(反之亦然);比使用 BufferedStream 等更快;
  • 它在内部使用基于数组的对象模型(Foo[] 而不是 List<Foo>),因为一旦构建完成大小就固定了,并且需要非常快。

但这确实是一个例外;对于一般的业务处理,List<T> 每次都赢过其他数据结构。


14
调整大小的争论是完全有效的。然而,即使不需要调整大小,人们仍更喜欢使用列表。对于后一种情况,是否存在一个坚实、合乎逻辑的论据,还是只是"数组已经过时"这种说法? - Frederick The Fool
16
在想要添加/删除数据时,一定要使用 List<T>,因为调整数组大小的成本很高。List<T> 内部使用了一个数组。你是不是想使用 LinkedList<T>? - dan-gph
31
更多的特性==更加复杂==不好,除非你需要这些特性。这个回答基本上列出了数组更好的原因,然而得出了相反的结论。 - Eamon Nerbonne
15
如果你不使用那些功能,我可以几乎保证它们不会对你造成任何影响...但是:根据我的经验,从不需要改变的集合数量远远小于那些被修改的集合。 - Marc Gravell
12
这取决于你的编码风格。根据我的经验,几乎没有集合被改变过。也就是说,集合从数据库中检索或从某些源构建,但进一步处理总是通过重新创建新集合来完成(例如map/filter等)。即使需要概念上的修改,最简单的方法也是生成一个新的集合。我只在性能优化时才会对集合进行修改,这样的优化往往高度本地化,不会向API消费者公开该修改。 - Eamon Nerbonne
显示剩余33条评论

139

我只是回答并添加一个链接,我很惊讶这个链接还没有被提到:Eric Lippert的博客文章"数组被认为有点危险。"

从标题可以判断出它建议在实际情况下尽可能使用集合 - 但正如Marc所指出的那样,有很多地方数组确实是唯一可行的解决方案。


25

除了其他推荐使用 List<T> 的答案之外,处理以下情况时您将需要使用数组:

  • 图像位图数据
  • 其他低级数据结构(即网络协议)

1
为什么要使用网络协议?难道您不想在此处使用自定义结构并为它们提供特殊的序列化程序或显式内存布局吗?此外,使用 List<T> 而不是字节数组有何不妥之处? - Konrad Rudolph
11
@Konrad - 首先,Stream.Read和Stream.Write使用byte[],同样适用于Encoding等... - Marc Gravell

15

除非你真的关心性能,我的意思是,“你为什么不用C++而使用.Net?”你应该坚持使用List<>。它更容易维护,并且可以在幕后为您调整数组大小。 (如果必要,List<> 在选择数组大小方面相当聪明,因此通常不需要您手动调整大小。)


22
为什么你使用.NET而不是C++?XNA。 - Bengt
2
为了阐述@Bengt的评论,性能并不是.NET的优先考虑因素,这在某种程度上是公平的。但是一些聪明的人想到了使用.NET与游戏引擎。事实上,现在大多数游戏引擎都使用C#。在这方面,Unity 3D是“当一个游戏引擎不是为AAA而设计时”的典范。 - mireazma
2
@mireazma,这个说法(关于Unity)已经很久不再有效了。我们通过HPC#和Burst从C#中获得了C++的性能。许多引擎内部正在迁移到C#。即使您在非DOTS项目中谈论游戏脚本,IL2CPP也可以非常好地生成高性能代码。 - 3Dave
@3Dave,我们不要在这个问题上开始争论。"IL2CPP非常出色地生成了高性能代码"。确实如此,在其范围内做得非常好。与2015年的https://www.jacksondunstan.com/articles/3001相反,我认为IL2CPP在性能方面与mono相比做得非常好。但与mono相比,与本机C++相比就是无稽之谈了。我在Unity中使用il2cpp进行算术比较测试,并在发布的exe中进行了所有优化,与C++相比,时间分别为18745、18487毫秒左右,Unity 2020中的其他时间也差不多,而C++则为189毫秒(有时约为36毫秒)。你可以自己进行测试。 - mireazma
@mireazma 我的评论更关注于Burst和HPC#,在这些方面我们看到的性能与等效本地代码一样好(有时甚至更好)。不过你的基准测试数据很有趣。我会研究一下。干杯。 - 3Dave

11

当集合本身的不可变性是客户端和提供程序代码之间契约的一部分(不一定是集合内部项的不可变性),并且IEnumerable不适用时,应优先使用数组而不是List。

例如:

var str = "This is a string";
var strChars = str.ToCharArray();  // returns array

很明显,“strChars”的修改不会改变原始的“str”对象,无论“str”底层类型的实现级别知识如何。

但是假设

var str = "This is a string";
var strChars = str.ToCharList();  // returns List<char>
strChars.Insert(0, 'X');
在这种情况下,仅凭代码片段就不清楚insert方法是否会改变原始的"str"对象。需要String的实现级别知识才能做出判断,这违反了按合同设计的方法。在String的情况下,这并不是什么大问题,但在几乎所有其他情况下都可能成为一个大问题。将List设置为只读确实有所帮助,但会导致运行时错误而不是编译时错误。

1
我对C#相对较新,但我不清楚为什么返回一个列表会暗示原始数据的可变性,而返回一个数组则不会。我认为,以“To”开头的方法将创建一个无法修改原始实例的对象,而“strChars as char[]”则表明您现在可以修改原始对象(如果有效)。 - Tim MB
@TimMB,这里有集合的不可变性(无法添加或删除项)和集合中项的不可变性。我指的是后者,而你可能混淆了两者。返回一个数组可以确保客户端无法添加/删除项。如果这样做,它会重新分配数组,并确保不会影响原始数据。返回列表时,没有做出这样的保证,原始数据可能会受到影响(取决于实现方式)。更改集合中的项(无论是数组还是列表),如果该项的类型不是结构体,则可能会影响原始数据。 - Herman Schoenfeld
谢谢您的澄清。我仍然感到困惑(可能是因为我来自C++世界)。如果 str 内部使用一个数组,而 ToCharArray 返回对该数组的引用,那么客户端可以通过更改该数组的元素来改变 str,即使大小保持不变。然而,您写道“很明显修改“strChars”不会改变原始的“str”对象”。我在这里错过了什么?从我所看到的情况来看,无论是哪种类型,客户端都可以访问内部表示形式,并且这将允许某种形式的突变。 - Tim MB

5

数组 vs. 列表是一个经典的可维护性 vs. 性能问题。几乎所有开发者都遵循的经验法则是,你应该追求两者,但当它们发生冲突时,选择可维护性而非性能。这个规则的例外是当性能已经被证明是一个问题时。如果将这个原则应用到数组 vs. 列表中,那么你会得到以下结论:

使用强类型列表,直到你遇到性能问题。如果你遇到性能问题,请决定是否放弃使用列表并转向使用数组,以使你的解决方案在性能方面获得更大的好处,而不会对其维护造成损害。


2
这涉及到我们必须面对的核心业务决策,时间就是金钱,数组通常在开发和维护方面比对象列表或集合花费更多的时间,特别是多维数组。但在许多情况下,它们会带来一些性能和内存损失的代价。 - Chris Schaller

5

如果我知道我需要多少个元素,比如说我需要5个元素,而且永远只需要这5个元素,那么我会使用一个数组。否则,我会使用List<T>。


1
在你知道元素数量的情况下,为什么不使用List<T>? - Oliver
就我个人而言,当我知道一个数组将包含x个元素并且永远只有x个元素时,我会使用数组(至少在内部)。这样做是为了在阅读代码时帮助区分可变集合和不可变集合。话虽如此,除非需要对列表进行外部修改,否则我对它们的大多数公共访问都是通过IEnumerable接口实现的。 - user3915050

3
大多数情况下,使用List足以满足需求。 List使用内部数组来处理其数据,并在向List添加比其当前容量更多的元素时自动调整数组大小,相比数组更易于使用,因为您需要预先知道其容量。
有关C#中列表的更多信息,请参见http://msdn.microsoft.com/en-us/library/ms379570(v=vs.80).aspx#datastructures20_1_topic5或反编译System.Collections.Generic.List<T>
如果需要多维数据(例如使用矩阵或图形编程),则可能会选择使用array
与往常一样,如果内存或性能是问题,请进行测量!否则,您可能会对代码做出错误的假设。

1
你好,能否解释一下为什么“列表的查找时间复杂度是O(n)”是正确的?据我所知,List<T>在后台使用数组。 - dragonfly
1
@dragonfly你说得完全正确。来源。当时,我认为实现使用了指针,但是后来我发现不是这样的。从上面的链接中可以看出:'检索此属性的值是O(1)操作;设置此属性也是O(1)操作。' - Sune Rievers

2

对于方法参数声明,我倾向于使用IReadOnlyList<MyClass>IList<MyClass>,这两者都可以接受数组和列表。例如:void Foo(IList<int> foo)可以像这样调用:Foo(new[] { 1, 2, 3 })Foo(new List<int> { 1, 2, 3 })

在C#中,数组是一个列表。MyClass[]List<MyClass>都实现了IList<MyClass>
因此,如果您正在编写一个接受List<MyClass>作为参数但仅使用其功能子集的方法,则声明为IList<MyClass>(或更严格的IReadOnlyList<MyClass>)可能更方便调用者。
详情:

1
在C#中,数组是一个列表这种说法是不正确的;数组并不是List,它只是实现了IList接口。 - Rufus L

1
.NET中的列表是数组的包装器,并在内部使用一个数组。列表上的操作的时间复杂度与数组相同,但由于所有添加的功能/易用性(例如自动调整大小和列表类提供的方法)存在一些额外开销。基本上,除非有强烈的理由不这样做(例如需要编写极其优化的代码或正在使用围绕数组构建的其他代码),否则我建议在所有情况下都使用列表。

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