在以下测试中:
int[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Func<int, int> boom = x => { Console.WriteLine(x); return x; };
var res = data.Select(boom).Skip(3).Take(4).ToList();
Console.WriteLine();
res.Select(boom).ToList();
结果是:
1
2
3
4
5
6
7
4
5
6
7
本质上,我观察到在这个例子中,Skip()
和Take()
工作得很好,Skip()
不像Take()那样懒惰。Skip()
似乎仍然枚举跳过的项,即使它不返回它们。
如果我先执行Take()
,同样适用。我的最佳猜测是,它至少需要列举第一次跳过或采取,以便查看下一次该去哪里。
它为什么要这样做?
Skip()
和Take()
都在IEnumerable<>
上运行。
IEnumerable<>
不支持提前跳过--它一次只能给您一个项目。考虑到这一点,你可以把Skip()
更多地看作一个过滤器——它仍然会接触到源序列中的所有项目,但它会过滤掉你告诉它的数量。重要的是,它会过滤它们,使它们不进入下一个项目,而不是前面的项目。
因此,通过这样做:
data.Select(boom).Skip(3)
在每个项目进入Skip()
过滤器之前,您正在对它们执行boom()
。
如果您改为这样,它将在Select
之前进行筛选,并且您将仅对剩余项目调用boom()
:
data.Skip(3).Take(4).Select(boom)
如果反编译Enumerable
,您将看到以下Skip
的实现:
while (count > 0 && e.MoveNext())
--count;
以及CCD_ 17的以下实现:
foreach (TSource source1 in source)
{
yield return source1;
if (--count == 0) break;
}
因此,这两个LINQ方法实际上都枚举了这些项。不同之处在于枚举项是否会被放置在结果集合中。IEnumerable
就是这样工作的。
它们都迭代集合。然后进行最后的收集。它就像一个简单的for loop
,里面有一个if else
条件
此外,您首先使用boom
选择它,它会打印集合的项目。
通过这个例子,您无法判断skip()
或take()
是否迭代了整个集合,但事实上,它们确实迭代了
假设我正确理解了你的问题。
起重臂func在跳过操作之前执行。
尝试选择数据后跳过和采取,你会得到确切的。
这种行为将来可能会改变,正如在本次关于Pull Request的讨论中所看到的,该讨论优化了实现IList接口的源的Skip方法的行为。
不是答案,但想想Skip(...)
将如何实现。这里有一种方法:
public static IEnumerable<T> Skip<T>(this IEnumerable<T> list, int count)
{
foreach (var item in list)
{
if (count > 0) count--;
else yield return item;
}
}
请注意,即使只返回一个子集,也会枚举整个list
参数。