Skip的性能(以及类似function,如Take)

我刚看了.NET Framework的Skip / Take扩展方法的源代码(在IEnumerable类型上),发现内部实现正在使用GetEnumerator方法:

 // .NET framework public static IEnumerable Skip(this IEnumerable source, int count) { if (source == null) throw Error.ArgumentNull("source"); return SkipIterator(source, count); } static IEnumerable SkipIterator(IEnumerable source, int count) { using (IEnumerator e = source.GetEnumerator()) { while (count > 0 && e.MoveNext()) count--; if (count <= 0) { while (e.MoveNext()) yield return e.Current; } } } 

假设我有一个包含1000个元素的IEnumerable (底层类型是List )。 如果我正在做list.Skip(990)会怎么样。接受(10)? 在采取最后十个元素之前,它是否会通过990首元素进行迭代? (这就是我的理解)。 如果是,那么我不明白为什么微软没有像这样实现Skip方法:

  // Not tested... just to show the idea public static IEnumerable Skip(this IEnumerable source, int count) { if (source is IList) { IList list = (IList)source; for (int i = count; i < list.Count; i++) { yield return list[i]; } } else if (source is IList) { IList list = (IList)source; for (int i = count; i < list.Count; i++) { yield return (T)list[i]; } } else { // .NET framework using (IEnumerator e = source.GetEnumerator()) { while (count > 0 && e.MoveNext()) count--; if (count <= 0) { while (e.MoveNext()) yield return e.Current; } } } } 

事实上,他们为Count方法做了这样的例子……

  // .NET Framework... public static int Count(this IEnumerable source) { if (source == null) throw Error.ArgumentNull("source"); ICollection collectionoft = source as ICollection; if (collectionoft != null) return collectionoft.Count; ICollection collection = source as ICollection; if (collection != null) return collection.Count; int count = 0; using (IEnumerator e = source.GetEnumerator()) { checked { while (e.MoveNext()) count++; } } return count; } 

那是什么原因?

在Jon Skeet重新实施Linq的优秀教程中,他(简要地)讨论了这个问题:

尽管大多数这些操作都无法进行合理优化,但在源实现IList时优化Skip是有意义的。 我们可以跳过跳过,可以这么说,直接找到适当的索引。 这不会发现在迭代之间修改源的情况,这可能是我所知道的在框架中没有实现的一个原因。

这似乎是推迟优化的合理理由,但我同意,对于特定情况,如果您可以保证您的源不能/不会被修改,那么进行优化可能是值得的。

正如ledbutter所提到的那样,当Jon Skeet 重新实现LINQ时 ,他提到像Skip这样的优化“不会发现在迭代之间修改源的情况”。 您可以将代码更改为以下内容,以便检查该情况。 它通过在集合的枚举器上调用MoveNext()来实现,即使它不使用e.Current ,因此如果集合发生更改,该方法将抛出。

当然,这消除了优化的重要部分:枚举器需要被创建,部分逐步通过和处理,但它仍然具有您无需毫无意义地逐步执行第一个count对象的好处。 你可能会感到困惑的是,你有一个e.Current ,因为它指向list[i - count]而不是list[i]

 public static IEnumerable Skip(this IEnumerable source, int count) { using (IEnumerator e = source.GetEnumerator()) { if (source is IList) { IList list = (IList)source; for (int i = count; i < list.Count; i++) { e.MoveNext(); yield return list[i]; } } else if (source is IList) { IList list = (IList)source; for (int i = count; i < list.Count; i++) { e.MoveNext(); yield return (T)list[i]; } } else { // .NET framework while (count > 0 && e.MoveNext()) count--; if (count <= 0) { while (e.MoveNext()) yield return e.Current; } } } } 

我假设他们想在另一个线程中同时修改底层集合时抛出InvalidOperationException “Collection was modified …”。 你的版本没有这样做。 它会产生可怕的结果。

这是MSFT在所有集合中贯穿整个.Net框架的标准做法,它不是线程安全的(虽然有些是特殊的)。