前段时间我编写了一个IList
扩展方法,通过使用索引来枚举列表的一部分。在重构时,我意识到可以通过调用Skip(toSkip).Take(amount)
来执行类似的查询。在对此进行基准测试时,我注意到Skip
未针对IList
进行优化。通过一些谷歌搜索,我最终在Jon Skeet帖子discussing why optimizing methods like Skip
is dangerous。
据我理解这篇文章,问题是在修改集合时抛出优化方法没有异常,但是作为注释声明msdn文档本身就会发生冲突。
如果对集合进行了更改, 例如添加,修改或删除 元素,枚举器是 无可挽回地失效和下一个 调用MoveNext或重置会抛出一个 InvalidOperationException异常。
在IEnumerator.GetEnumerator()中:
如果对集合进行了更改, 例如添加,修改或删除 元素,枚举器是 不可恢复的无效及其 行为未定义。
我认为这两种惯例都很有用,无论是否进行优化,我都有点失落。什么是正确的解决方案?我一直在考虑Kris Vandermotten在评论中提到的IList.AssumeImmutable()
方法AsParallel()
。是否存在任何实现,或者这是一个坏主意?
答案 0 :(得分:2)
我同意Rafe的观点,即未定义的行为更为正确。只有版本化的集合才能抛出异常而不是所有集合都是版本化的(数组是最大的例子)。如果您在调用MoveNext
之间进行了2 ^ 32次更改,即使是版本化的集合也可能会出现异常。
假设您真的关心版本控制行为,解决方案是为Enumerator
获取IList
并在每次迭代时调用MoveNext
:
public static IEnumerable<T> Skip<T>(this IList<T> source, int count)
{
using (var e = source.GetEnumerator())
while (count < source.Count && e.MoveNext())
yield return source[count++];
}
这样,您可以通过索引获得O(1)行为,但仍然可以获得调用MoveNext
的所有异常抛出行为。请注意,我们仅针对异常副作用调用MoveNext
;我们忽略了它所枚举的值。
答案 1 :(得分:0)
ReadOnlyCollection类可能有助于您的不可变集合。
我的建议:除非遇到性能问题,否则我个人不会试图“欺骗”编译器。您永远不会知道,下一个版本可能会使您的优化代码运行速度比原始代码慢两倍。不要抢先优化。框架中提供的方法可以产生一些非常优化的代码,这些代码很难重新实现。
here是来自msdn的文章,提供了有关用于不同目的的集合的信息。我会使用适当的集合来完成任务,而不是尝试优化Skip and Take。