【问题标题】:How does IEnumerable<T>.ToArray() work?IEnumerable<T>.ToArray() 如何工作?
【发布时间】:2011-05-19 09:51:59
【问题描述】:

它是两遍算法吗?即,它迭代可枚举一次以计算元素的数量,以便它可以分配数组,然后再次传递以插入它们?

它是否循环一次,并不断调整数组的大小?

或者它是否使用像 List 这样的中间结构(可能在内部调整数组的大小)?

【问题讨论】:

  • 我的建议是下载 .NET Reflector 并自己查看源代码。
  • @Justin 仅使用 Microsoft 的框架参考源。他们往往有 cmets 和更好的变量名 :-) 这是我用来研究我的答案的。

标签: c# linq arrays


【解决方案1】:

它使用中间结构。实际涉及的类型是 Buffer,它是框架中的内部结构。实际上,这种类型有一个数组,每次它满时都会复制它以分配更多空间。这个数组从长度 4 开始(在 .NET 4 中,它是一个可能会改变的实现细节),所以你在做 ToArray 时可能最终会分配和复制很多。

不过,有一个优化。如果源实现了ICollection&lt;T&gt;,它使用 Count 来从一开始就分配正确大小的数组。

【讨论】:

  • 最坏情况下分配的临时空间总量将是结果数组大小的三倍,每个项目最多写入一次。如果溢出数组意味着为新数据分配两倍大的数组(将旧数据留在原处),则所需的总空间可以减少到数组大小的两倍,但即使是 3 倍也不是很大。请注意,虽然有些项目可能会被复制十几次或更多次,但平均值永远不会超过 3 次。
  • supercat:注意该算法仍然使用 O(n) 空间和时间。
  • 为什么不让它变得不安全,利用堆栈分配,这将显着提升临时对象?
  • @supercat 你是怎么得出这些数字的?你能举个例子吗?
  • @Backwards_Dave:每次缓冲区填满时,一半的项目将被写入一次(在最后一次填满之后),一半将从前一个缓冲区复制。前一个缓冲区中的一半项目将被写入一次,一半来自较早的缓冲区,等等。就在缓冲区填满后,新缓冲区中的一半项目将被复制,其中一个被写入一次。
【解决方案2】:

首先它检查源是否为ICollection&lt;T&gt;,在这种情况下它可以调用源的ToArray() 方法。

否则,它将仅枚举一次源。在枚举时,它将项目存储到缓冲区数组中。每当它到达缓冲区数组的末尾时,它就会创建一个两倍大小的新缓冲区并复制旧元素。枚举完成后,它会返回缓冲区(如果它的大小正好合适)或将缓冲区中的项目复制到一个大小正好合适的数组中。

这里是操作的伪源代码:

public static T[] ToArray<T>(this IEnumerable<T> source)
{
    T[] items = null;
    int count = 0;

    foreach (T item in source)
    {
        if (items == null)
        {
            items = new T[4];
        }
        else if (items.Length == count)
        {
            T[] destinationArray = new T[count * 2];
            Array.Copy(items, 0, destinationArray, 0, count);
            items = destinationArray;
        }
        items[count] = item;
        count++;
    }

    if (items.Length == count)
    {
        return items;
    }
    T[] destinationArray = new TElement[count];
    Array.Copy(items, 0, destinationArray, 0, count);
    return destinationArray;
}

【讨论】:

    【解决方案3】:

    像这样(通过 .NET Reflector):

    public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        Buffer<TSource> buffer = new Buffer<TSource>(source);
        return buffer.ToArray();
    }
    
    [StructLayout(LayoutKind.Sequential)]
    internal struct Buffer<TElement>
    {
        internal TElement[] items;
        internal int count;
        internal Buffer(IEnumerable<TElement> source)
        {
            TElement[] array = null;
            int length = 0;
            ICollection<TElement> is2 = source as ICollection<TElement>;
            if (is2 != null)
            {
                length = is2.Count;
                if (length > 0)
                {
                    array = new TElement[length];
                    is2.CopyTo(array, 0);
                }
            }
            else
            {
                foreach (TElement local in source)
                {
                    if (array == null)
                    {
                        array = new TElement[4];
                    }
                    else if (array.Length == length)
                    {
                        TElement[] destinationArray = new TElement[length * 2];
                        Array.Copy(array, 0, destinationArray, 0, length);
                        array = destinationArray;
                    }
                    array[length] = local;
                    length++;
                }
            }
            this.items = array;
            this.count = length;
        }
    
        internal TElement[] ToArray()
        {
            if (this.count == 0)
            {
                return new TElement[0];
            }
            if (this.items.Length == this.count)
            {
                return this.items;
            }
            TElement[] destinationArray = new TElement[this.count];
            Array.Copy(this.items, 0, destinationArray, 0, this.count);
            return destinationArray;
        }
    }
    

    【讨论】:

      【解决方案4】:

      一般来说,尝试对可枚举对象进行两次迭代可能会导致灾难,因为无法保证对可枚举对象进行第二次迭代。因此,执行Count 然后分配然后复制就不行了。

      在 Reflector 中,它表明它使用了一种名为 Buffer 的类型,该类型有效地将序列流式传输到一个数组中,根据需要调整大小(每次重新分配时加倍,以便重新分配的数量为 O(log n)),然后返回适当大小的到达终点时的数组

      【讨论】:

        【解决方案5】:

        首先,将项目加载到允许生成计数的内部类Buffer&lt;T&gt;

        接下来,调用Buffer&lt;T&gt;.ToArray,它将Buffer&lt;T&gt; 的数组中的Array.Copy 放入返回的数组中。

        .NET Reflector 会显示此代码,如果您想亲自查看。

        http://www.red-gate.com/products/reflector/

        【讨论】:

          猜你喜欢
          • 2010-11-06
          • 1970-01-01
          • 2011-09-27
          • 2016-07-16
          • 2023-03-10
          • 1970-01-01
          • 2015-04-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多