C# Tips | コレクション・LINQ:ページング

C# C#
スポンサーリンク

はじめに:「ページング」は“データを一口サイズに切り分ける技”

業務システムの一覧画面で、
「1ページ 20件」「次へ」「前へ」といった UI をよく見ますよね。
これが「ページング」です。

データは何千件・何万件あっても、
人間が一度に見られるのはせいぜい数十件。
だから「今は全体のうち、このページの分だけを取り出す」という考え方が必要になります。

C# と LINQ では、SkipTake を組み合わせることで、
とてもシンプルにページングが書けます。
ここから、初心者向けに「考え方 → コード → 実務での注意点」の順でかみ砕いていきます。


ページングの基本概念をコードに落とす

ページ番号・ページサイズ・開始位置

ページングで必ず出てくるのが、この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 を作る

毎回 SkipTake を手で書くのは面倒なので、
「ページ番号とページサイズを渡すと、そのページだけ返してくれる」拡張メソッドを作っておくと便利です。

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 を見て「次へ」「前へ」のボタン制御ができます。


実務で意識してほしいポイント

ページングは「ソートとセット」で考える

ページングだけを先にやってしまうと、
ページごとに並び順がバラバラになり、ユーザーが混乱します。

必ず、

  1. 並び順を決める(OrderBy / ThenBy)
  2. その結果に対してページングする(Skip / Take)

という順番で書く癖をつけてください。

0 件のときも破綻しない設計にする

データが 0 件のときでも、

  • TotalCount = 0
  • TotalPages = 0
  • Items は空のリスト

のように、例外ではなく「空の結果」として扱えるようにしておくと、
画面側のコードもシンプルになります。

DB 側ページングとの違いも意識しておく

今回の例は「メモリ上のコレクションに対するページング」でしたが、
実務では「DB クエリに対するページング(SQL の OFFSET/FETCH など)」もよく使います。

考え方は同じで、

  • 並び順を決める
  • スキップ件数と取得件数を決める

という流れです。
LINQ to Entities(EF など)でも OrderBySkipTake の順で書くのは同じなので、
ここでの感覚はそのまま活きます。


まとめ:「ページングユーティリティ」は“人間が扱えるサイズにデータを切るナイフ”

ページングの本質は、
「大量のデータを、ページという単位に切り分けて、
今必要な部分だけを取り出す」ことです。

押さえておきたいポイントは、

  • Skip((pageNumber - 1) * pageSize)Take(pageSize) の組み合わせが基本形
  • ソート → ページングの順番を守る
  • 総件数・総ページ数を含めた PagedResult<T> のような型にしておくと、画面側が楽になる

このあたりを自分のユーティリティとして持っておけば、
「とりあえず全部取ってきてから画面で頑張る」ような力技から卒業して、
“業務・実務で通用するページング処理”を落ち着いて書けるようになります。

タイトルとURLをコピーしました