如何在C#中使用Linq Skip和Take实现负页码的分页功能
实现C#中支持负页码的Linq分页功能
刚好做过类似的需求,咱们可以通过封装Linq扩展方法来优雅实现这个负页码分页逻辑——核心思路就是把负页码转换成对应的正页码,再用Skip和Take完成分页。
先理清楚规则:
- 负页码代表从数据集末尾倒数的页码,比如总记录15、每页5条时,
-1对应最后一页(第3页),-2对应倒数第二页(第2页),以此类推 - 如果负页码的绝对值超过总页数,自动 fallback 到第一页;正页码超过总页数则 fallback 到最后一页
封装扩展方法
我写了两个版本,分别适配IEnumerable(内存集合)和IQueryable(EF Core等ORM场景,避免提前全表扫描):
public static class PaginationExtensions { // 针对内存集合的版本 public static IEnumerable<T> Paginate<T>(this IEnumerable<T> source, int pageIndex, int pageSize) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize), "每页大小必须大于0"); var totalRecords = source.Count(); var totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); int actualPage; if (pageIndex < 0) { // 把负页码转为正页码:总页数 + 负页码 + 1 actualPage = totalPages + pageIndex + 1; // 确保页码不小于1,避免绝对值过大的情况 actualPage = Math.Max(actualPage, 1); } else { actualPage = pageIndex; // 确保页码不超过总页数 actualPage = Math.Min(actualPage, totalPages); } if (totalRecords == 0) return Enumerable.Empty<T>(); var skipCount = (actualPage - 1) * pageSize; skipCount = Math.Max(skipCount, 0); return source.Skip(skipCount).Take(pageSize); } // 针对IQueryable的版本(适合EF Core等ORM,生成SQL分页语句) public static IQueryable<T> Paginate<T>(this IQueryable<T> source, int pageIndex, int pageSize) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize), "每页大小必须大于0"); var totalRecords = source.Count(); var totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); int actualPage; if (pageIndex < 0) { actualPage = totalPages + pageIndex + 1; actualPage = Math.Max(actualPage, 1); } else { actualPage = pageIndex; actualPage = Math.Min(actualPage, totalPages); } if (totalRecords == 0) return source.Take(0); var skipCount = (actualPage - 1) * pageSize; skipCount = Math.Max(skipCount, 0); return source.Skip(skipCount).Take(pageSize); } }
示例用法
用你提到的场景测试一下:
// 模拟15条记录(1到15) var records = Enumerable.Range(1, 15); int pageSize = 5; // 获取最后一页(页码-1) var lastPage = records.Paginate(-1, pageSize); // 结果:11,12,13,14,15 → 对应Skip(10).Take(5) // 获取倒数第二页(页码-2) var secondLastPage = records.Paginate(-2, pageSize); // 结果:6,7,8,9,10 → 对应Skip(5).Take(5) // 获取倒数第三页(页码-3) var thirdLastPage = records.Paginate(-3, pageSize); // 结果:1,2,3,4,5 → 对应Skip(0).Take(5) // 边界测试:负页码绝对值超过总页数(总页数3) var fallbackToFirstPage = records.Paginate(-4, pageSize); // 结果:1-5
关键逻辑说明
- 总页数计算:用
Math.Ceiling向上取整,确保不满一页的记录也单独算一页(比如16条记录、每页5条,总页数是4) - 负页码转换:公式
totalPages + pageIndex + 1的逻辑是把负页码映射到从后往前的正页码,比如总页数3,-1→3,-2→2,完美匹配需求 - 边界防护:处理了空集合、非法页大小、页码超出范围等异常情况,保证方法的健壮性
内容的提问来源于stack exchange,提问作者Sridhar
相关产品推荐
相关产品推荐

