はじめに:「ページング」は“データを一口サイズに切り分ける技”
業務システムの一覧画面で、
「1ページ 20件」「次へ」「前へ」といった UI をよく見ますよね。
これが「ページング」です。
データは何千件・何万件あっても、
人間が一度に見られるのはせいぜい数十件。
だから「今は全体のうち、このページの分だけを取り出す」という考え方が必要になります。
C# と LINQ では、Skip と Take を組み合わせることで、
とてもシンプルにページングが書けます。
ここから、初心者向けに「考え方 → コード → 実務での注意点」の順でかみ砕いていきます。
ページングの基本概念をコードに落とす
ページ番号・ページサイズ・開始位置
ページングで必ず出てくるのが、この3つです。
- ページ番号(pageNumber)
- 1ページあたりの件数(pageSize)
- どこから何件取るか(Skip と Take)
例えば「1ページ 10件」で「3ページ目」を表示したいとき、
「先頭から 20件飛ばして、次の 10件を取る」というイメージになります。
数式にするとこうです。
スキップする件数 = (pageNumber - 1) * pageSize
3ページ目なら (3 - 1) * 10 = 20 件スキップ、ということですね。
基本形:Skip と Take でページングする
シンプルな例で動きを確認する
まずは単純な数値のリストで、ページングの動きを見てみます。
using System;
using System.Collections.Generic;
using System.Linq;
var numbers = Enumerable.Range(1, 50).ToList(); // 1〜50
int pageNumber = 2; // 2ページ目
int pageSize = 10;
int skip = (pageNumber - 1) * pageSize;
var page = numbers
.Skip(skip)
.Take(pageSize);
Console.WriteLine($"Page {pageNumber}:");
foreach (var n in page)
{
Console.WriteLine(n);
// 11〜20 が出力される
}
C#ここでの重要ポイントは、
「Skip は“前から何件捨てるか”、Take は“そこから何件取るか”」という役割だということです。
この2つを組み合わせるだけで、任意のページを切り出せます。
ページングをユーティリティメソッドにする
汎用的な拡張メソッド Paged を作る
毎回 Skip と Take を手で書くのは面倒なので、
「ページ番号とページサイズを渡すと、そのページだけ返してくれる」拡張メソッドを作っておくと便利です。
public static class PagingExtensions
{
public static IEnumerable<T> Paged<T>(
this IEnumerable<T> source,
int pageNumber,
int pageSize)
{
if (pageNumber < 1) throw new ArgumentOutOfRangeException(nameof(pageNumber));
if (pageSize < 1) throw new ArgumentOutOfRangeException(nameof(pageSize));
int skip = (pageNumber - 1) * pageSize;
return source
.Skip(skip)
.Take(pageSize);
}
}
C#使い方はこうなります。
var numbers = Enumerable.Range(1, 50).ToList();
foreach (var n in numbers.Paged(pageNumber: 3, pageSize: 10))
{
Console.WriteLine(n); // 21〜30
}
C#ここでの重要ポイントは、
「ページ番号は 1 始まりとして扱い、0 やマイナスは例外にする」など、
“ルールをユーティリティ側で決めてしまう”ことです。
呼び出し側は「何ページ目を何件ずつで取りたいか」だけを意識すればよくなります。
実務っぽい例:社員一覧をページングする
データモデルとダミーデータ
社員クラスを用意します。
public class Employee
{
public int No { get; set; }
public string Name { get; set; } = "";
}
C#ダミーデータを作ります。
var employees = Enumerable.Range(1, 53)
.Select(i => new Employee { No = i, Name = $"User{i:D3}" })
.ToList();
C#ソートしてからページングするのが基本
一覧画面では、
「社員番号順」「名前順」など、
まず“並び順”を決めてからページングするのが普通です。
int pageNumber = 2;
int pageSize = 20;
var page = employees
.OrderBy(e => e.No) // 並び順を決める
.Paged(pageNumber, pageSize); // そのあとページング
C#出力してみます。
Console.WriteLine($"Page {pageNumber}:");
foreach (var e in page)
{
Console.WriteLine($"{e.No}: {e.Name}");
}
C#ここでの重要ポイントは、
「ソート → ページング」の順番を守ることです。
逆にすると、「ページごとに並び順がバラバラ」という悲しいことになります。
総件数・総ページ数も一緒に扱う
ページング結果をまとめて返すクラス
実務では、
「今のページのデータ」だけでなく、
- 総件数(TotalCount)
- 総ページ数(TotalPages)
- 今が何ページ目か(PageNumber)
なども一緒に返したくなります。
それをまとめる小さなクラスを作っておきましょう。
public sealed class PagedResult<T>
{
public IReadOnlyList<T> Items { get; }
public int TotalCount { get; }
public int PageNumber { get; }
public int PageSize { get; }
public int TotalPages { get; }
public PagedResult(IEnumerable<T> items, int totalCount, int pageNumber, int pageSize)
{
Items = items.ToList();
TotalCount = totalCount;
PageNumber = pageNumber;
PageSize = pageSize;
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
}
}
C#PagedResult を返すユーティリティ
public static class PagingExtensions
{
public static PagedResult<T> ToPagedResult<T>(
this IEnumerable<T> source,
int pageNumber,
int pageSize)
{
if (pageNumber < 1) throw new ArgumentOutOfRangeException(nameof(pageNumber));
if (pageSize < 1) throw new ArgumentOutOfRangeException(nameof(pageSize));
int totalCount = source.Count();
int skip = (pageNumber - 1) * pageSize;
var items = source
.Skip(skip)
.Take(pageSize);
return new PagedResult<T>(items, totalCount, pageNumber, pageSize);
}
}
C#使い方はこうです。
var result = employees
.OrderBy(e => e.No)
.ToPagedResult(pageNumber: 3, pageSize: 20);
Console.WriteLine($"Page {result.PageNumber}/{result.TotalPages} (Total: {result.TotalCount})");
foreach (var e in result.Items)
{
Console.WriteLine($"{e.No}: {e.Name}");
}
C#ここでの重要ポイントは、
「ページング結果を“ただの IEnumerable<T>”ではなく、“ページ情報付きのオブジェクト”として扱う」ことです。
これで、画面側は TotalPages を見て「次へ」「前へ」のボタン制御ができます。
実務で意識してほしいポイント
ページングは「ソートとセット」で考える
ページングだけを先にやってしまうと、
ページごとに並び順がバラバラになり、ユーザーが混乱します。
必ず、
- 並び順を決める(OrderBy / ThenBy)
- その結果に対してページングする(Skip / Take)
という順番で書く癖をつけてください。
0 件のときも破綻しない設計にする
データが 0 件のときでも、
- TotalCount = 0
- TotalPages = 0
- Items は空のリスト
のように、例外ではなく「空の結果」として扱えるようにしておくと、
画面側のコードもシンプルになります。
DB 側ページングとの違いも意識しておく
今回の例は「メモリ上のコレクションに対するページング」でしたが、
実務では「DB クエリに対するページング(SQL の OFFSET/FETCH など)」もよく使います。
考え方は同じで、
- 並び順を決める
- スキップ件数と取得件数を決める
という流れです。
LINQ to Entities(EF など)でも OrderBy → Skip → Take の順で書くのは同じなので、
ここでの感覚はそのまま活きます。
まとめ:「ページングユーティリティ」は“人間が扱えるサイズにデータを切るナイフ”
ページングの本質は、
「大量のデータを、ページという単位に切り分けて、
今必要な部分だけを取り出す」ことです。
押さえておきたいポイントは、
Skip((pageNumber - 1) * pageSize)とTake(pageSize)の組み合わせが基本形- ソート → ページングの順番を守る
- 総件数・総ページ数を含めた
PagedResult<T>のような型にしておくと、画面側が楽になる
このあたりを自分のユーティリティとして持っておけば、
「とりあえず全部取ってきてから画面で頑張る」ような力技から卒業して、
“業務・実務で通用するページング処理”を落ち着いて書けるようになります。
