我经常遇到这样的情况:我想在声明查询的地方对查询进行求值。这通常是因为我需要对它进行多次迭代,计算成本很高。例如:

string raw = "...";
var lines = (from l in raw.Split('\n')
             let ll = l.Trim()
             where !string.IsNullOrEmpty(ll)
             select ll).ToList();

这很好。但是如果我不打算修改结果,那么我也可以调用ToArray()而不是ToList()。

然而,我想知道ToArray()是否通过首先调用ToList()来实现,因此内存效率比只调用ToList()低。

我疯了吗?我是否应该调用ToArray() -在知道内存不会被分配两次的情况下安全可靠?


当前回答

首选ToListAsync<T>()。

在实体框架6中,这两个方法最终都调用相同的内部方法,但ToArrayAsync<T>()在最后调用list.ToArray(),实现为

T[] array = new T[_size];
Array.Copy(_items, 0, array, 0, _size);
return array;

所以ToArrayAsync<T>()有一些开销,因此ToListAsync<T>()是首选。

其他回答

(七年后……)

其他几个(好的)答案集中在将会发生的微观性能差异上。

这篇文章只是一个补充,以提及由数组(T[])产生的IEnumerator<T>与由List<T>返回的IEnumerator之间存在的语义差异。

最好用例子来说明:

IList<int> source = Enumerable.Range(1, 10).ToArray();  // try changing to .ToList()

foreach (var x in source)
{
  if (x == 5)
    source[8] *= 100;
  Console.WriteLine(x);
}

上面的代码将毫无例外地运行,并产生输出:

1
2
3
4
5
6
7
8
900
10

这表明int[]返回的IEnumarator<int>并不跟踪自枚举器创建以来数组是否被修改过。

Note that I declared the local variable source as an IList<int>. In that way I make sure the C# compiler does not optimze the foreach statement into something which is equivalent to a for (var idx = 0; idx < source.Length; idx++) { /* ... */ } loop. This is something the C# compiler might do if I use var source = ...; instead. In my current version of the .NET framework the actual enumerator used here is a non-public reference-type System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32] but of course this is an implementation detail.

现在,如果我将.ToArray()改为.ToList(),我只得到:

1
2
3
4
5

其次是一个系统。InvalidOperationException爆炸说:

修改集合;枚举操作可能无法执行。

在这种情况下,底层枚举器是公共可变值类型System. collections . generic . list ' 1+ enumerator [System. collections . generic . list]。Int32](在这种情况下,在IEnumerator<int>框内,因为我使用IList<int>)。

综上所述,List<T>生成的枚举数跟踪列表在枚举过程中是否发生变化,而T[]生成的枚举数则没有。因此,在. tolist()和. toarray()之间进行选择时,请考虑此差异。

人们经常添加一个额外的. toarray()或. tolist()来绕过一个在枚举器的生命周期内跟踪它是否被修改的集合。

(如果有人想知道List<>如何跟踪集合是否被修改,这个类中有一个私有字段_version,每当List<>被更新时,它都会被更改。实际上可以通过简单地删除索引器public T this[int index]的set访问器中增加_version的行来改变List<>的这种行为,就像最近在Dictionary<,>中所做的那样,如另一个答案所述。)

性能差异并不显著,因为List<T>是作为动态大小的数组实现的。调用ToArray()(它使用内部Buffer<T>类来增长数组)或ToList()(它调用List<T>(IEnumerable<T>)构造函数)将最终成为将它们放入数组并增长数组直到适合它们为止的问题。

如果您希望具体确认这一事实,请查看Reflector中所讨论的方法的实现——您将看到它们的代码几乎完全相同。

我同意@mquander的观点,性能差异应该是微不足道的。但是,我想对它进行基准测试,所以我这样做了——结果是微不足道的。

Testing with List<T> source:
ToArray time: 1934 ms (0.01934 ms/call), memory used: 4021 bytes/array
ToList  time: 1902 ms (0.01902 ms/call), memory used: 4045 bytes/List

Testing with array source:
ToArray time: 1957 ms (0.01957 ms/call), memory used: 4021 bytes/array
ToList  time: 2022 ms (0.02022 ms/call), memory used: 4045 bytes/List

每个源数组/列表有1000个元素。所以你可以看到时间和记忆的差异都可以忽略不计。

我的结论是:您还可以使用ToList(),因为List<T>提供了比数组更多的功能,除非几个字节的内存确实对您很重要。

除非您只是需要一个数组来满足其他约束,否则您应该使用ToList。在大多数情况下,ToArray会比ToList分配更多的内存。

两者都使用数组进行存储,但是ToList有一个更灵活的约束。它需要数组至少与集合中的元素数量一样大。如果数组更大,这不是问题。但是ToArray需要数组的大小精确到元素的数量。

为了满足这个约束,ToArray通常比ToList多做一次分配。一旦它有了一个足够大的数组,它就会分配一个完全正确大小的数组,并将元素复制回该数组中。唯一可以避免这种情况的情况是当数组的增长算法恰好与需要存储的元素数量一致时(绝对是少数)。

EDIT

有几个人问我在List<T>值中有额外的未使用内存的后果。

这是一个合理的担忧。如果创建的集合寿命很长,在创建后从未被修改过,并且有很高的机会落在Gen2堆中,那么您可能会更好地预先分配额外的ToArray。

总的来说,我发现这种情况比较罕见。更常见的情况是,大量ToArray调用被立即传递给其他短期内存使用,在这种情况下,ToList显然更好。

这里的关键是分析,分析,再分析更多。

内存总是会被分配两次——或者类似的情况。由于不能调整数组的大小,这两种方法都将使用某种机制在不断增长的集合中收集数据。(好吧,这个名单本身就是一个不断增长的集合。)

List使用数组作为内部存储,并在需要时将容量增加一倍。这意味着平均2/3的项目至少被重新分配过一次,其中一半至少被重新分配过两次,一半至少被重新分配过三次,以此类推。这意味着每个项目平均被重新分配了1.3次,这并不是很大的开销。

还要记住,如果你在收集字符串,集合本身只包含对字符串的引用,字符串本身不会被重新分配。