System.Linq.Enumerable.Reverse是否将内部的所有元素复制到数组中?

时间:2012-02-18 00:24:25

标签: .net linq .net-4.0

几年前,somebody complained about the implementation of Linq.Reverse() and Microsoft promised to fix that。这是在2008年,所以问题是,框架4是否有Linq.Reverse()的优化实现,当集合类型允许时,它不会实现集合(即将所有元素复制到内部数组)(例如{{1} }})?

2 个答案:

答案 0 :(得分:13)

显然,无法优化所有案例。如果某个对象仅实现IEnumerable<T>而不是IList<T>,则必须迭代它直到结束才能找到最后一个元素。因此,优化仅针对实施IList<T>的类型(例如T[]List<T>)。

现在, 它在.Net 4.5 DP中实际优化了吗?让我们启动 Reflector ILSpy:

public static IEnumerable<TSource> Reverse<TSource>(
    this IEnumerable<TSource> source)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    return ReverseIterator<TSource>(source);
}

好的,ReverseIterator<TSource>()看起来怎么样?

private static IEnumerable<TSource> ReverseIterator<TSource>(
    IEnumerable<TSource> source)
{
    Buffer<TSource> buffer = new Buffer<TSource>(source);
    for (int i = buffer.count - 1; i >= 0; i--)
    {
        yield return buffer.items[i];
    }
    yield break;
}

迭代器块的作用是为集合创建Buffer<T>并向后迭代。我们几乎就在那里,Buffer<T>是什么?

[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;
    }

    // one more member omitted
}

我们在这里有什么?我们将内容复制到数组中。在每种情况下。唯一的优化是,如果我们知道Count(即集合实现ICollection<T>),我们就不必重新分配数组。

因此,IList<T>的优化在.Net 4.5 DP中。它会在每种情况下创建整个集合的副本。

如果我猜测为什么它没有被优化,在阅读Jon Skeet's article on this issue之后,我认为这是因为优化是可观察的。如果在迭代时改变集合,您将看到更改后的数据与优化,但没有它的旧数据。实际上以微妙的方式改变某些行为的优化是一件坏事,因为它具有向后兼容性。

答案 1 :(得分:1)

编辑:是的,似乎已进行此更改

您链接的错误报告将错误标记为已修复,但我想确保自己。所以,我写了这个小程序:

static void Main(string[] args)
{
    List<int> bigList = Enumerable.Range(0, 100000000).ToList();

    Console.WriteLine("List allocated");
    Console.ReadKey();

    foreach (int n in bigList.Reverse<int>())
    {
        // This will never be true, but the loop ensures that we enumerate
        // through the return value of Reverse()
        if (n > 100000000)
            Console.WriteLine("{0}", n);
    }
}

这个想法是程序将400 MB的空间分配到bigList,然后等待用户按下一个键,然后通过扩展方法语法调用Enumerable.Reverse(bigList)

我在Windows 7 x64计算机上使用Debug版本测试了这个程序。根据任务经理的说法,在启动程序之前我的内存使用量正好是2.00 GB。然后,在我按键之前,内存使用量达到2.63 GB。点击密钥后,内存使用率会短暂上升到2.75 GB。但重要的是,它不会超过400 MB或更多,如果Enumerable.Reverse()正在制作副本,就会出现这种情况。

原始帖子

在某些情况下,正确的Enumerable.Reverse()实现不能复制到数组或其他数据结构。

您链接的投诉仅与IList<T>进行交易。但是,在一般情况下,我认为Enumerable.Reverse() 必须将元素复制到某个内部缓冲区。

考虑以下方法

private int x = 0;

public IEnumerable<int> Foo()
{
    for (int n = 0; n < 1000; n++)
    {
        yield return n;
        x++;
    }
}

现在让我们说Enumerable.Reverse()在这种情况下没有将输入IEnumerable<T>复制到缓冲区。然后,循环

foreach (int n in Foo().Reverse())
    Console.WriteLine("{0}", n);

将一直遍历迭代器块以获取第一个n,一直遍历前999个元素以获得第二个n,依此类推。但是这对x和前向迭代的影响不会相同,因为每次迭代几乎一直到x的返回值时,我们都会改变Foo()。为了防止正向和反向迭代之间的这种脱节,Enumerable.Reverse()方法必须复制输入IEnumerable<T>