获取页面并合并到页面列表

本文关键字:列表 获取 合并 | 更新日期: 2023-09-27 18:29:17

我有一个列表,假设它包含1000个项目。我想得到一个包含 10 乘以 100 个项目的列表,如下所示:

myList.Select(x => x.y).Take(100) (until list is empty)
所以我希望 Take(100( 运行十次,因为列表包含 1000 个项目,最终得到包含 10 个列表

的列表,每个列表包含 100 个项目。

获取页面并合并到页面列表

您需要跳过已经记录的记录数,您可以跟踪此数字并在查询时使用它

alreadyTaken = 0;
while (alreadyTaken < 1000) {
    var pagedList = myList.Select(x => x.y).Skip(alreadyTaken).Take(100);
    ...
    alreadyTaken += 100;
}

这可以通过简单的分页扩展方法实现。

public static List<T> GetPage<T>(this List<T> dataSource, int pageIndex, int pageSize = 100)
{
    return dataSource.Skip(pageIndex * pageSize)
        .Take(pageSize)
        .ToList();
}

当然,您可以扩展它以接受和/或返回任何类型的IEnumerable<T>

如前所述,您可以使用 for 循环并Skip一些元素并Take一些元素。通过这种方式,您可以在每个for循环中创建一个新查询。但是,如果您还想遍历每个查询,则会出现问题,因为这将非常低效。假设您只有 50 个条目,并且您想遍历每个循环包含 10 个元素的列表。您将有 5 个循环执行

  1. .跳过(0(.竹(10(
  2. .跳过(10(.竹(10(
  3. .跳过(20(.竹(10(
  4. .跳过(30(.竹(10(
  5. .跳过(40(.竹(10(

这里提出了两个问题。

  1. Skip元素仍可能导致计算。在第一个查询中,您只计算所需的 10 个元素,但在第二个循环中,您计算了 20 个元素并丢弃了 10 个元素,依此类推。如果将所有 5 个循环相加,则即使只有 50 个元素,您也已经计算了 10 + 20 + 30 + 40 + 50 = 150 个元素。这会导致 O(n^2( 性能。
  2. 并非每个IEnumerable都执行上述操作。例如,一些IEnumerable数据库可以优化Skip,例如它们在SQL查询中使用Offset(MySQL(定义。但这仍然不能解决问题。您仍然遇到的主要问题是您将创建 5 个不同的查询并执行所有 5 个查询。这五个查询现在将花费最多的时间。因为对数据库的简单查询甚至比跳过一些内存中元素或一些计算要慢得多。

由于所有这些问题,如果您还想评估每个循环中的每个查询,则不使用具有多个.Skip(x).Take(y)for 循环是有意义的。相反,您的算法应该只遍历一次 IEnumerable,执行一次查询,并在第一次迭代时返回前 10 个元素。下一次迭代返回接下来的 10 个元素,依此类推,直到元素用完为止。

以下扩展方法正是这样做的。

public static IEnumerable<IReadOnlyList<T>> Combine<T>(this IEnumerable<T> source, int amount) {
    var combined = new List<T>();
    var counter  = 0;
    foreach ( var entry in source ) {
        combined.Add(entry);
        if ( ++counter >= amount ) {
            yield return combined;
            combined = new List<T>();
            counter  = 0;
        }
    }
    if ( combined.Count > 0 )
        yield return combined;
}

有了这个,你可以做

someEnumerable.Combine(100)

你会得到一个新的IEnumerable<IReadOnlyList<T>>,它只通过一次枚举,将所有内容切成最多 100 个元素的块。

只是为了显示性能可能有多大差异:

var numberCount  = 100000;
var combineCount = 100;
var nums  = Enumerable.Range(1, numberCount);
var count = 0;
// Bechmark with Combine() Extension
var swCombine  = Stopwatch.StartNew();
var sumCombine = 0L;
var pages      = nums.Combine(combineCount);
foreach ( var page in pages ) {
    sumCombine += page.Sum();
    count++;
}
swCombine.Stop();
Console.WriteLine("Count: {0} Sum: {1} Time Combine: {2}", count, sumCombine, swCombine.Elapsed);
// Doing it with .Skip(x).Take(y)
var swTakes = Stopwatch.StartNew();
count = 0;
var sumTaken = 0L;
var alreadyTaken = 0;
while ( alreadyTaken < numberCount ) {
    sumTaken += nums.Skip(alreadyTaken).Take(combineCount).Sum();
    alreadyTaken += combineCount;
    count++;
}
swTakes.Stop();
Console.WriteLine("Count: {0} Sum: {1} Time Takes: {2}", count, sumTaken, swTakes.Elapsed);

Combine(( 扩展方法的使用在我的计算机上以3 milliseconds运行 (i5 @ 4Ghz(,而for循环已经需要178 milliseconds

如果你有更多的元素或切片更小,情况会变得更糟。例如,如果combineCount设置为10而不是100则运行时将更改为4 milliseconds1800 milliseconds (1.8 seconds)

现在你可以说你没有那么多元素,或者你的切片永远不会变得这么小。但请记住,在这个例子中,我只是生成了一个计算时间几乎为零的数字序列。从4 milliseconds178 milliseconds的全部开销仅由重新评估和Skip值引起的。如果你在幕后有一些更复杂的事情发生,跳过会产生最大的开销,而且如果IEnumerable可以实现Skip,比如上面解释的数据库,这个例子仍然会变得更糟,因为最大的开销将是查询本身的执行。

而且查询量可以非常快地增加。使用 100.000 个元素和 100 个切片/分块,您已经执行 1.000 个查询。另一方面,上面提供的Combine扩展将始终执行一次查询。并且永远不会遭受上述任何问题。


所有这些都并不意味着应该避免SkipTake。他们有自己的位置。但是,如果您真的打算遍历每个元素,则应避免使用SkipTake来完成切片。

例如,如果您唯一想要的只是将所有内容切成包含 100 个元素的页面,并且您只想获取第三页。您只需要计算需要跳过多少元素。

var pageCount = 100;
var pageNumberToGet = 3;
var thirdPage = yourEnumerable.Skip(pageCount * (pageNumberToGet-1)).take(pageCount);

通过这种方式,您将在单个查询中获取从200300的元素。此外,带有 databse 的 IEnumerable 可以优化它,并且您只有一个查询。因此,如果您只想要IEnumerable的特定范围的元素,那么您应该使用 SkipTake 并像上面一样进行操作,而不是使用我提供的 Combine 扩展方法。