调用ToList()会对性能产生影响吗?

179
在使用ToList()时,需要考虑性能影响吗?
我正在编写一个查询以从目录中检索文件,查询如下: string[] imageArray = Directory.GetFiles(directory); 然而,由于我喜欢使用List<>,因此我决定尝试使用以下代码: List<string> imageList = Directory.GetFiles(directory).ToList(); 那么,在决定进行此类转换时是否应该考虑某种性能影响 - 还是仅在处理大量文件时才需要考虑?这是一次微不足道的转换吗?

我也很感兴趣知道这里的答案。在我看来,除非应用程序对性能要求极高,否则如果使用List<T>可以使代码更加逻辑清晰、易读和易于维护,我会始终选择它,而不是T[](当然,如果转换确实导致了明显的性能问题,那么我可能会重新考虑)。 - Sepster
从数组创建列表应该非常便宜。 - leppie
2
@Sepster 我只会尽可能地指定需要完成工作的数据类型。如果我不必调用 AddRemove,我会将其保留为 IEnumerable<T>(或者更好的是 var)。 - p.s.w.g
4
在这种情况下,我认为最好调用EnumerateFiles而不是GetFiles,这样只会创建一个数组。 - tukaef
如果您在目录中有很多文件,请参考此答案:https://dev59.com/cmsz5IYBdhLWcg3wfn0x#7865180 - Haris Hasan
3
目前在.NET中实现的GetFiles(directory),基本上是执行了new List<string>(EnumerateFiles(directory)).ToArray()。因此,GetFiles(directory).ToList()会创建一个列表,然后从该列表创建一个数组,最后再次创建一个列表。正如2kay所说,这里应该优先考虑执行EnumerateFiles(directory).ToList() - Joren
10个回答

217

IEnumerable<T>.ToList()

是的,IEnumerable<T>.ToList() 会对性能产生影响,它是一个 O(n) 操作,但通常只需要在性能关键操作中注意。

ToList() 操作将使用 List(IEnumerable<T> collection) 构造函数。这个构造函数必须复制数组(更一般地说,是 IEnumerable<T>),否则原始数组的未来修改将在源 T[] 上改变,这通常是不可取的。

我想再次强调,这只有在大型列表中才会有差异,复制内存块是一个相当快的操作。

小贴士:As vs To

您会注意到,在 LINQ 中有几个以 As(例如 AsEnumerable())和 To(例如 ToList())开头的方法。以 To 开头的方法需要像上面那样进行转换(即可能影响性能),而以 As 开头的方法则不需要,只需要一些强制转换或简单操作。

List<T> 的其他细节

以下是有关 List<T> 工作原理的更多详细信息,如果您感兴趣的话 :)

List<T> 也使用了一个称为动态数组的构造,需要根据需要调整大小,此调整大小事件将旧数组的内容复制到新数组中。因此,它从小开始,如果需要,则增加大小

这是关于 CapacityCount 属性在 List<T> 中的区别。 Capacity 是指后台数组的大小,而 Count 是指 List<T> 中的项数,它始终小于或等于 Capacity。因此,当向列表中添加一项并将其增加到超过 Capacity 时,List<T> 的大小会加倍并复制数组。

3
我想强调一下 List(IEnumerable<T> collection) 构造函数会检查参数 collection 是否为 ICollection<T>,如果是,则立即创建所需大小的新内部数组。如果参数 collection 不是 ICollection<T>,则构造函数会迭代它,并对每个元素调用 Add 方法。 - Justinas Simanavicius
4
需要注意的是,您经常会发现ToList()是一个具有误导性的操作。这种情况通常出现在您通过LINQ查询创建IEnumerable<>时。LINQ查询被构建但尚未执行。调用ToList()将运行查询,因此似乎会消耗大量资源 - 但实际上是查询本身消耗了资源,而不是ToList()操作(除非它是一个真正巨大的列表)。 - dancer42

42

调用 toList() 会对性能产生影响吗?

当然会。理论上,即使是 i++ 也会对程序产生性能影响,可能会使程序变慢几个时钟周期。

.ToList 做了什么?

当调用 .ToList 时,代码将调用 Enumerable.ToList(),它是一个扩展方法,return new List<TSource>(source)。在相应的构造函数中,在最坏的情况下,它会遍历项目容器并逐个将其添加到新容器中。因此,它对性能影响很小。它不可能成为您应用程序的性能瓶颈。

问题代码有什么问题

Directory.GetFiles 遍历文件夹并立即将所有文件名返回到内存中,这存在潜在风险,即字符串数组可能占用大量内存,导致一切变慢。

那么应该怎么做

这取决于情况。如果您(以及业务逻辑)保证文件夹中的文件数量始终很少,则可以接受此代码。但仍建议使用延迟版本:C#4 中的 Directory.EnumerateFiles。这更像是一个查询,不会立即执行,可以在其上添加更多查询,例如:

Directory.EnumerateFiles(myPath).Any(s => s.Contains("myfile"))

当找到文件名包含 "myfile" 的文件时,它将立即停止搜寻路径。这显然比 .GetFiles 有更好的性能。


23
调用 toList() 方法会对性能产生影响。使用扩展方法 Enumerable.ToList() 将从 IEnumerable 源集合构造一个新的 List 对象,这当然会对性能产生影响。但是,了解 List 可以帮助您确定性能影响是否显著。
List 使用数组 (T[]) 存储列表的元素。一旦分配了数组,就无法扩展它们,因此 List 将使用超大数组来存储列表的元素。当 List 增长到超过底层数组的大小时,必须分配一个新的数组,并将旧数组的内容复制到新的更大的数组中,然后列表才能增长。
当从 IEnumerable 构造新的 List 时,有两种情况:
1.源集合实现 ICollection:然后使用 ICollection.Count 获取源集合的确切大小,并分配一个匹配的后备数组,然后使用 ICollection.CopyTo() 将源集合的所有元素复制到后备数组中。该操作非常高效,可能会映射到某些 CPU 指令,用于复制内存块。但是,就性能而言,需要为新数组提供内存,并且需要为复制所有元素提供 CPU 循环。
2.否则,源集合的大小未知,枚举器 IEnumerator 用于将每个源元素逐个添加到新的 List 中。最初,后备数组为空,并且创建大小为 4 的数组。然后,当此数组太小时,将其大小加倍,因此后备数组的增长方式如下:4、8、16、32 等等。每次后备数组增长时,都必须重新分配并复制到目前为止存储的所有元素。与第一种情况相比,这个操作要昂贵得多,因为无法立即创建正确大小的数组。
此外,如果源集合包含例如 33 个元素,则列表最终将使用 64 个元素的数组,浪费了一些内存。
在您的情况下,源集合是实现 ICollection 的数组,因此性能影响不是您应该担心的问题,除非您的源数组非常大。调用 ToList() 只会复制源数组并将其包装在一个 List 对象中。即使对于小集合,第二种情况的性能也不值得担心。

6
这将和以下操作一样(低)效率:
var list = new List<T>(items);

如果您反汇编接受 IEnumerable<T> 参数的构造函数的源代码,您会看到它会执行以下几个步骤:
  • 调用 collection.Count,如果 collection 是一个 IEnumerable<T>,它将强制执行。如果 collection 是一个数组、列表等,则应为 O(1)

  • 如果 collection 实现了 ICollection<T>,则使用 ICollection<T>.CopyTo 方法将项目保存在内部数组中。它应该是 O(n),其中 n 是集合的长度。

  • 如果 collection 没有实现 ICollection<T>,则遍历集合的项,并将它们添加到内部列表中。

所以,是的,它会消耗更多的内存,因为它必须创建一个新的列表,在最坏的情况下,它将是 O(n),因为它将遍历 collection 来复制每个元素。


3
关闭,0(n) 其中 n 是原始集合中字符串占用的总字节数,而不是元素的数量(更准确地说,n = 字节/字大小)。 - user1416420
@user1416420 我可能错了,但是为什么呢?如果它是其他类型的集合(例如 boolint 等),会怎样呢?你不需要复制集合中的每个字符串,只需将它们添加到新列表中即可。 - Oscar Mederos
仍然无论如何,新的内存分配和字节复制是导致该方法失效的原因。在.NET中,一个布尔值也会占用4个字节。实际上,在.NET中,每个对象的引用至少有8个字节长,因此它非常慢。前4个字节指向类型表,后4个字节指向值或内存位置以查找该值。 - user1416420

5
ToList()方法会创建一个新的List并将其中的元素放入,这意味着使用ToList()会有相关的成本。对于小集合来说,成本不会很明显,但是如果集合很大,则会影响性能。
通常情况下,除非转换集合为List是必须的,否则不应使用ToList()。例如,如果你只想遍历该集合,则无需执行ToList。
如果你正在针对数据源执行查询操作,例如使用LINQ to SQL连接数据库,那么使用ToList的成本会更高,因为当您使用ToList与LINQ to SQL时,它不会执行Delayed Execution即需要时才加载项目(在许多场景中可能会有好处),而是立即将项目从数据库加载到内存中。

Haris:我不确定在调用ToList()后原始源会发生什么。 - TalentTuner
@Saurabh GC会清理它 - p.s.w.g
@Saurabh 原始源代码不会受到任何影响。新创建的列表将引用原始源代码的元素。 - Haris Hasan
如果你只想遍历集合,就不需要执行ToList操作 - 那么你应该如何进行迭代呢? - SharpC

5
“是否需要考虑性能影响?”
对于您的具体情况,首先最关心的是硬盘速度和缓存效率。从这个角度来看,影响肯定可以忽略不计,所以答案是不需要考虑。
但仅仅当您真正需要使用结构的特性来提高生产力、优化算法或获得其他优势时才这样做。否则,只是无谓地增加了微不足道的性能损耗。自然而然地,您不应该这么做!:)

3
考虑到获取文件列表的性能,ToList()是可以忽略不计的。但对于其他场景则未必如此。这取决于你在哪里使用它。
当调用数组、列表或其他集合时,您将创建一个该集合的副本作为 List<T>。这里的性能取决于列表的大小。只有在真正需要时才应这样做。
在您的示例中,您在数组上调用它。它会遍历整个数组并逐个将项目添加到新创建的列表中。因此,性能影响取决于文件数量。
当在 IEnumerable<T> 上调用时,您需要将该 IEnumerable<T>(通常是查询)实例化

2
ToList会创建一个新的列表,并将元素从原始源复制到新创建的列表中,因此唯一需要做的就是从原始源复制元素,这取决于源大小。

0

让我们看另一个例子;

如果您正在处理数据库,当您运行ToList()方法并检查此代码的SQL Profiler时;

var IsExist = (from inc in entities.be_Settings                                
 where inc.SettingName == "Number"                                
 select inc).ToList().Count > 0;

自动生成的查询将会像这样:

SELECT [Extent1].[SettingName] AS [SettingName], [Extent1].[SettingValue] AS [SettingValue] FROM [dbo].[be_Settings] AS [Extent1] WHERE N'Number' = [Extent1].[SettingName]

使用ToList方法运行选择查询,并将查询结果存储在内存中,通过查看List的元素数量来检查是否存在记录。例如,如果您的表中有符合相关条件的1000条记录,则首先从数据库中获取这1000条记录并转换为对象,然后将它们放入一个List中,只需检查此List的元素数量即可。因此,这是一种非常低效的选择方式。


-1

这并不完全是关于列表性能的问题,但如果您有高维数组,可以使用 HashSet 而不是 List。


你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

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