Array 与 List<T>:何时使用哪个?

Posted

技术标签:

【中文标题】Array 与 List<T>:何时使用哪个?【英文标题】:Array versus List<T>: When to use which? 【发布时间】:2010-09-30 21:21:29 【问题描述】:
MyClass[] array;
List<MyClass> list;

在哪些情况下一种优于另一种?为什么?

【问题讨论】:

数组已经过时了,如popular discussion 这里所示。还有pointed out here,我们的主持人in the blog。 如果我没记错的话 List 有一个数组作为内部结构。每当内部数组被填充时,它只需将内容复制到一个两倍大小的数组(或其他一些常数乘以当前大小)。 en.wikipedia.org/wiki/Dynamic_array Ykok:你说的好像对,我找到了List<> here的源代码。 @gimel 认为数组已经过时可能有点大胆 【参考方案1】:

实际上,您很少会想要使用数组。任何时候你想添加/删除数据时都一定要使用List&lt;T&gt;,因为调整数组大小很昂贵。如果您知道数据是固定长度的,并且您想出于某些非常具体的原因(在基准测试之后)进行微优化,那么数组可能会很有用。

List&lt;T&gt; 提供了比数组更多的功能很多(尽管 LINQ 稍微平衡了一点),而且几乎总是正确的选择。当然,params 参数除外。 ;-p

作为计数器 - List&lt;T&gt; 是一维的;其中,您有像 int[,]string[,,] 这样的矩形(等)数组 - 但还有其他方法可以在对象模型中对此类数据进行建模(如果需要)。

另见:

How/When to abandon the use of Arrays in c#.net? Arrays, What's the point?

也就是说,我在protobuf-net 项目中大量使用了数组;完全是为了性能:

它会进行大量位移,因此byte[] 对编码非常重要; 我使用本地滚动byte[] 缓冲区,在向下发送到底层流(和 v.v.)之前填充该缓冲区;比BufferedStream 等更快; 它在内部使用基于数组的对象模型(Foo[] 而不是List&lt;Foo&gt;),因为一旦构建,大小就固定了,并且需要非常快。

但这绝对是个例外;对于一般业务线处理,List&lt;T&gt; 每次都会获胜。

【讨论】:

关于调整大小的论点是完全正确的。然而,即使不需要调整大小,人们也更喜欢列表。对于后一种情况,是否有一个可靠的、合乎逻辑的论点,还是仅仅是“数组过时了”? “任何时候你想添加/删除数据都一定要使用 List,因为调整数组大小很昂贵。” List 在内部使用数组。你在想 LinkedList 吗? 更多功能 == 更复杂 == 不好,除非您需要这些功能。这个答案基本上列出了数组更好的原因,但得出了相反的结论。 @EamonNerbonne 如果您不使用这些功能,我几乎可以保证它们不会伤害您……但是:不需要突变的集合数量是 以我的经验,比那些变异的要小得多 @MarcGravell:这取决于你的编码风格。根据我的经验,几乎没有任何集合发生过变异。那是;集合是从数据库中检索或从某个源构建的,但始终通过重新创建新集合(例如 map/filter 等)来完成进一步的处理。即使在需要概念突变的情况下,生成一个新集合往往也是最简单的。我只对集合进行变异作为性能优化,并且这种优化往往是高度本地化的,不会将变异暴露给 API 使用者。【参考方案2】:

真的只是回答添加一个我很惊讶的链接还没有被提及:Eric 的 Lippert 在"Arrays considered somewhat harmful." 上的博客条目

您可以从标题中判断出它建议在任何可行的地方使用集合 - 但正如 Marc 正确指出的那样,在很多地方数组确实是唯一实用的解决方案。

【讨论】:

三年多后终于有时间阅读这篇文章了哈哈。好文章,现在好文章。 :)【参考方案3】:

尽管其他答案推荐 List&lt;T&gt;,但您在处理时仍需要使用数组:

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

【讨论】:

为什么要使用网络协议?您不想在这里使用自定义结构并给它们一个特殊的序列化程序或显式内存布局吗?此外,这里有什么反对使用List&lt;T&gt; 而不是字节数组? @Konrad - 好吧,对于初学者来说,Stream.Read 和 Stream.Write 与 byte[] 一起使用,Encoding 等也是如此......【参考方案4】:

除非你真的关心性能,我的意思是,“你为什么使用 .Net 而不是 C++?”你应该坚持使用 List。它更易于维护,并为您完成了在后台调整数组大小的所有繁琐工作。 (如有必要,List 在选择数组大小方面非常聪明,因此通常不需要。)

【讨论】:

"你为什么使用 .Net 而不是 C++?" XNA 详细说明@Bengt 评论,性能不是.NET 的优先事项,这绝对是公平的。但是一些聪明的人想到了将 .NET 与游戏引擎一起使用。事实上,现在大多数游戏引擎都使用 C#。在这方面,Unity 3D 是“当游戏引擎不适合 AAA 时”的范例。 @mireazma 很长一段时间以来都不是有效的声明(wrt Unity)。我们通过 HPC# 和 Burst 从 C# 中获得 C++ 性能。许多引擎内部正在迁移到 C#。即使您谈论的是非 DOTS 项目中的游戏脚本,IL2CPP 在生成高性能代码方面也做得非常非常好。 @3Dave 我们不要就此展开争论。 “IL2CPP 在生成高性能代码方面做得非常非常好”。确实如此,在其影响范围内。与jacksondunstan.com/articles/3001(2015 年)相反,我认为 IL2CPP 与单声道相比在性能方面做得非常好。但与单声道相比。与本机 C++ 相比,这是无稽之谈。我在 Unity 中使用 il2cpp 在 released exe 中进行了算术比较测试,所有优化与 C++ 相比,Unity 2020 中的时间分别为 18745、18487ms 和更多时间,而 C++ 中的时间为 189ms(有时约为 36ms )。自己做一个测试。 @mireazma 我的评论旨在更关注 Burst 和 HPC#,我们认为性能与等效的本机代码一样好(在某些情况下更好)。不过,您的基准测试数字很有趣。我会调查的。干杯。【参考方案5】: 当集合本身的不变性是客户端和提供者代码之间合同的一部分(不一定是集合中的项目的不变性)并且当 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');

在这种情况下,仅从 code-sn-p 并不清楚插入方法是否会改变原始的“str”对象。它需要 String 的实现级别知识才能做出决定,这打破了按合同设计的方法。在 String 的情况下,这没什么大不了的,但在几乎所有其他情况下它都可能是个大问题。将列表设置为只读确实有帮助,但会导致运行时错误,而不是编译时错误。

【讨论】:

我对 C# 比较陌生,但我不清楚为什么返回列表会暗示原始数据的可变性,而返回数组则不会。我会认为名称以To 开头的方法将创建一个无法修改原始实例的对象,而不是strChars as char[],如果有效,则表明您现在可以修改原始实例对象。 @TimMB 集合的不变性(不能添加或远程项目)和集合中的项目的不变性。我指的是后者,而您可能将两者混为一谈。返回数组可确保客户端无法添加/删除项目。如果是这样,它会重新分配数组并确保它不会影响原始数组。返回一个列表,不做这样的保证,原始可能会受到影响(取决于实施)。如果项目的类型不是结构,则更改集合中的项目(无论是数组还是列表)可能会影响原始项目。 感谢您的澄清。我仍然很困惑(可能是因为我来自 C++ 世界)。如果str 内部使用了一个数组并且ToCharArray 返回了对该数组的引用,那么客户端可以通过更改该数组的元素来改变str,即使大小保持不变。然而你写了'很明显,“strChars”的修改不会改变原始的“str”对象'。我在这里想念什么?据我所见,在任何一种情况下,客户端都可以访问内部表示,并且无论类型如何,这都会允许某种突变。【参考方案6】:

如果我确切地知道我需要多少个元素,比如我需要 5 个元素并且只有 ever 5 个元素,那么我会使用一个数组。否则我只使用 List.

【讨论】:

在知道元素个数的情况下为什么不使用 List【参考方案7】:

大多数时候,使用List 就足够了。 List 使用内部数组来处理其数据,并在向List 添加的元素超过其当前容量时自动调整数组大小,这比您需要知道容量的数组更易于使用提前。

有关 C# 中的列表的更多信息,请参阅 http://msdn.microsoft.com/en-us/library/ms379570(v=vs.80).aspx#datastructures20_1_topic5 或直接反编译 System.Collections.Generic.List&lt;T&gt;

如果您需要多维数据(例如使用矩阵或图形编程),您可能会使用array

与往常一样,如果内存或性能是一个问题,请测量它!否则你可能会对代码做出错误的假设。

【讨论】:

嗨,你能解释一下为什么“一个列表的查找时间是 O(n)”是真的吗?据我所知 List 在幕后使用数组。 @dragonfly 你是完全正确的。 Source。当时,我假设实现使用了指针,但后来我学到了其他方法。从上面的链接:'检索此属性的值是 O(1) 操作;设置属性也是一个 O(1) 操作。'【参考方案8】:

数组与。列表是一个经典的可维护性与性能问题。几乎所有开发人员都遵循的经验法则是,您应该同时兼顾两者,但是当它们发生冲突时,请选择可维护性而不是性能。该规则的例外情况是性能已经被证明是一个问题。如果你把这个原则带到 Arrays Vs.列表,那么你得到的是这样的:

使用强类型列表,直到遇到性能问题。如果您遇到性能问题,请决定退出阵列是否会在性能方面对您的解决方案有益,而不是在维护方面对您的解决方案不利。

【讨论】:

【参考方案9】:

另一种尚未提及的情况是当一个人有大量项目时,每个项目都由一组固定的相关但独立的变量粘在一起(例如,点的坐标,或 3d 的顶点)三角形)。暴露字段结构的数组将允许其元素被有效地“就地”修改——这是任何其他集合类型都无法实现的。因为结构数组在 RAM 中连续保存其元素,所以对数组元素的顺序访问可以非常快。在代码需要对数组进行多次顺序传递的情况下,结构数组的性能可能比数组或其他类对象引用集合的性能高出 2:1;此外,就地更新元素的能力可能使结构数组的性能优于任何其他类型的结构集合。

虽然数组不可调整大小,但让代码存储数组引用以及正在使用的元素数量并根据需要将数组替换为更大的数组并不难。或者,可以轻松地为一种行为类似于List&lt;T&gt; 但暴露其后备存储的类型编写代码,从而允许使用MyPoints.Add(nextPoint);MyPoints.Items[23].X += 5;。请注意,如果代码尝试访问列表末尾之外的内容,后者不一定会引发异常,但在概念上的用法与 List&lt;T&gt; 非常相似。

【讨论】:

你描述的是一个列表。有一个索引器,因此您可以直接访问底层数组,并且 List 将为您维护大小。 @Carl:给定例如Point[] arr;,代码可以说,例如arr[3].x+=q;。使用例如List&lt;Point&gt; list,有必要改为说Point temp=list[3]; temp.x+=q; list[3]=temp;。如果List&lt;T&gt; 有一个方法Update&lt;TP&gt;(int index, ActionByRefRef&lt;T,TP&gt; proc, ref TP params) 会很有帮助。并且编译器可以将list[3].x+=q; 转换为list.Update(3, (ref int value, ref int param)=&gt;value+=param, ref q);,但不存在这样的功能。 好消息。有用。 list[0].X += 3; 将为列表的第一个元素的 X 属性添加 3。 listList&lt;Point&gt;Point 是具有 X 和 Y 属性的类【参考方案10】:

.NET 中的列表是数组的包装器,并在内部使用数组。列表操作的时间复杂度与数组相同,但是所有添加的功能/列表的易用性(例如自动调整大小和列表类附带的方法)都会产生更多开销。几乎,我建议在所有情况下都使用列表,除非有令人信服的理由这样做,例如,如果您需要编写极其优化的代码,或者正在使用其他围绕构建的代码数组。

【讨论】:

【参考方案11】:

因为没有人提到:在 C# 中,数组就是一个列表。 MyClass[]List&lt;MyClass&gt; 都实现了 IList&lt;MyClass&gt;。 (例如 void Foo(IList&lt;int&gt; foo) 可以被称为 Foo(new[] 1, 2, 3 )Foo(new List&lt;int&gt; 1, 2, 3 )

因此,如果您正在编写一个接受 List&lt;MyClass&gt; 作为参数但仅使用部分功能的方法,您可能需要声明为 IList&lt;MyClass&gt;,以方便调用者。

详情:

Why array implements IList? How do arrays in C# partially implement IList<T>?

【讨论】:

“在 C# 中,数组就是列表” 这不是真的;数组不是List,它只实现了IList接口。【参考方案12】:

与其比较每种数据类型的特性,我认为最实用的答案是“差异对于您需要完成的工作可能并不那么重要,特别是因为它们都实现了IEnumerable,所以遵循流行的惯例并使用List,直到你有理由不这样做,此时你可能有理由使用数组而不是List。”

在托管代码中的大部分时间,您会希望集合尽可能易于使用,而不是担心微优化。

【讨论】:

【参考方案13】:

它们可能不受欢迎,但我是游戏项目中的数组的粉丝。 - 在某些情况下,迭代速度可能很重要,如果您对每个元素做的不多,则数组上的 foreach 开销会显着减少 - 使用辅助函数添加和删除并不难 - 它的速度较慢,但​​在您只构建一次的情况下可能无关紧要 - 在大多数情况下,浪费的额外内存更少(仅对结构数组非常重要) - 稍微减少垃圾和指针和指针追逐

话虽如此,我在实践中使用 List 的次数远多于 Arrays,但它们各有所长。

如果 List 有一个内置类型,那就太好了,这样他们就可以优化包装器和枚举开销。

【讨论】:

【参考方案14】:

填充列表比填充数组更容易。对于数组,您需要知道数据的确切长度,但对于列表,数据大小可以是任意的。并且,您可以将列表转换为数组。

List<URLDTO> urls = new List<URLDTO>();

urls.Add(new URLDTO() 
    key = "wiki",
    url = "https://...",
);

urls.Add(new URLDTO()

    key = "url",
    url = "http://...",
);

urls.Add(new URLDTO()

    key = "dir",
    url = "https://...",
);

// convert a list into an array: URLDTO[]
return urls.ToArray();

【讨论】:

【参考方案15】:

请记住,使用 List 无法做到这一点:

List<string> arr = new List<string>();

arr.Add("string a");
arr.Add("string b");
arr.Add("string c");
arr.Add("string d");

arr[10] = "new string";

它会产生一个异常。

用数组代替:

string[] strArr = new string[20];

strArr[0] = "string a";
strArr[1] = "string b";
strArr[2] = "string c";
strArr[3] = "string d";

strArr[10] = "new string";

但是对于数组,没有自动调整数据结构的大小。您必须手动或使用 Array.Resize 方法来管理它。

一个技巧可以是用一个空数组初始化一个 List。

List<string> arr = new List<string>(new string[100]);

arr[10] = "new string";

但是在这种情况下,如果您使用 Add 方法放置一个新元素,它将被注入到 List 的末尾。

List<string> arr = new List<string>(new string[100]);

arr[10] = "new string";

arr.Add("bla bla bla"); // this will be in the end of List

【讨论】:

【参考方案16】:

这完全取决于需要数据结构的上下文。例如,如果您正在创建项目以供其他功能或服务使用,则使用 List 是完成它的完美方式。

现在,如果您有一个项目列表并且只想显示它们,比如说在网页数组上是您需要使用的容器。

【讨论】:

如果您有一个项目列表并且只想显示它们,那么仅使用已有的列表有什么问题?数组在这里能提供什么? 对于“创建供其他功能或服务使用的项目”,实际上,我更喜欢带有IEnumerable&lt;T&gt; 的迭代器块 - 然后我可以流式传输对象而不是缓冲它们。

以上是关于Array 与 List<T>:何时使用哪个?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 泛型 Array<T> 导致“不能将 T 用作具体类型参数。请改用类”但 List<T> 不会

无GC提取List<T>对应Array方法分析

无GC提取List<T>对应Array方法分析

无GC提取List<T>对应Array方法分析

无GC提取List<T>对应Array方法分析

何时在 F# 中使用序列而不是列表?