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