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

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() -在知道内存不会被分配两次的情况下安全可靠?


当前回答

(七年后……)

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

这篇文章只是一个补充,以提及由数组(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中所讨论的方法的实现——您将看到它们的代码几乎完全相同。

如果在IEnumerable<T>(例如,来自ORM)上使用ToList(),则通常是首选。如果序列的长度在开始时不知道,ToArray()会创建动态长度的集合(如List),然后将其转换为数组,这将花费额外的时间。

(七年后……)

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

这篇文章只是一个补充,以提及由数组(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<,>中所做的那样,如另一个答案所述。)

您应该根据理想的设计选择来决定使用ToList还是ToArray。如果您想要一个只能通过索引迭代和访问的集合,请选择ToArray。如果您希望以后能够轻松地从集合中添加和删除额外的功能,那么可以使用ToList(并不是说您不能添加到数组中,但这通常不是合适的工具)。

如果性能很重要,您还应该考虑哪些操作会更快。实际上,您不会调用ToList或ToArray一百万次,但可能会对获得的集合进行一百万次操作。在这方面[]更好,因为List<>是[],有一些开销。查看这个线程的一些效率比较:List<int>或int[]

在我自己不久前的测试中,我发现ToArray更快。我不确定这些测试有多偏颇。然而,性能差异是如此微不足道,只有在循环运行这些查询数百万次时才能明显看出。

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

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

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