4

我正在阅读一个问题,询问在 LINQ 查询中调用 ToList() 还是 ToArray() 更好吗?并发现自己想知道为什么Enumerable.ToArray()不首先调用该Count()方法来查找集合的大小,而不是使用Buffer{T}动态调整自身大小的内部类。类似于以下内容:

T[] ToArray<T>(IEnumerable<T> source)
{
    var count = source.Count();
    var array = new T[count];

    int index = 0;
    foreach (var item in source) array[index++] = item;
    return array;
}

我知道我们无法理解设计者和实施者的想法,我相信他们比我聪明得多。所以问这个问题的最好方法是上面显示的方法有什么问题?它似乎减少了内存分配,并且仍然在 O(n) 时间内运行。

4

4 回答 4

4

该类Buffer<T>针对源序列实现的情况进行了优化ICollection<T>

internal Buffer(IEnumerable<TElement> source)
{
   int length = 0;
   TElement[] array = null;
   ICollection<TElement> collection = source as ICollection<TElement>;
   if (collection != null)
   {
      length = collection.Count;
      if (length > 0)
      {
         array = new TElement[length];
         collection.CopyTo(array, 0);
      }
   }
   else
   {
      ...

如果序列未实现ICollection<T>,则代码不能假定枚举序列两次是安全的,因此它会根据需要重新调整数组的大小。

于 2013-06-18T15:53:35.170 回答
4

首先,Buffer<T>如果指定的序列可以转换为ICollection具有Count属性的(如数组或列表),类构造函数也会进行优化:

TElement[] array = null;
int num = 0;
ICollection<TElement> collection = source as ICollection<TElement>;
if (collection != null)
{
    num = collection.Count;
    if (num > 0)
    {
        array = new TElement[num];
        collection.CopyTo(array, 0);
    }
}
else
    // now we are going the long way ...

因此,如果它不是一个集合,则必须执行查询以获取总计数。但是Enumerable.Count仅使用正确大小的数组来初始化可能会非常昂贵,而且——更重要的是——可能会产生危险的副作用。因此它是不安全的。

考虑这个简单的File.ReadLines例子:

var lines = File.ReadLines(path);
int count = lines.Count(); // executes the query which also disposes the underlying IO.TextReader 
var array = new string[count];
int index = 0;
foreach (string line in lines) array[index++] = line;

这将引发ObjectDisposedException“无法从关闭的 TextReader 读取”,因为lines.Count()已经执行了查询,同时阅读器位于foreach.

于 2013-06-18T15:53:55.980 回答
1

因为 Count() 将源枚举到最后。因此它至少会进行 2 次迭代,一次仅用于计数,另一次用于复制项目。

现在考虑有问题的可枚举是数据库游标或其他类似的东西,在迭代时涉及非平凡的操作。那将是一个性能杀手。

仅 memcopy 和扩展缓冲区是一个更好的主意。它可能会对性能产生轻微影响,但它非常小,更重要的是它是一个已知数量

于 2013-06-18T15:46:56.943 回答
0

如果IEnumerable<T>和/或IEnumerator<T>包含了一个属性来询问它是否“知道”它的计数,以及一个属性,那么使用这样的东西Count可能是值得的[在例如调用在线程安全的可变类型上将枚举快照]。如果没有这种能力,即使代码有or ,它也无法知道调用是否会比创建额外的临时数组花费更多或更少的时间。ToArray()CountIEnumerator<T>GetEnumeratorICollectionICollection<T>Count

话虽如此,我希望诸如此类的最佳实现ToArray可能是使用事物的链接列表,每个事物都包含一定数量的项目,以便将每个项目读入它所占用的空间,直到这样时间,因为它可以复制到最终数组。的加倍策略在List<T>这里似乎不是特别合适,因为在多个小数组中划分信息比创建超过 85,000 字节的数组更好(因为临时数组在退出后将无用,让它们结束在大对象堆上会特别糟糕)。

于 2013-06-18T16:25:36.613 回答