前回は、LINQ to Objects のフィルター、射影、並べ替えを見ました。
今回は、ページング、件数取得、集合演算、重複除去、集計処理を整理します。
これらは、画面表示、検索結果、レポート作成などでよく使う操作です。
サンプルデータ
この記事では、次の商品データを使います。
class Product
{
public string Name { get; }
public string Category { get; }
public int Price { get; }
public bool InStock { get; }
public Product(string name, string category, int price, bool inStock)
{
Name = name;
Category = category;
Price = price;
InStock = inStock;
}
}
List<Product> products = new List<Product>
{
new Product("ノート", "文房具", 200, true),
new Product("ペン", "文房具", 150, false),
new Product("キーボード", "PC", 4500, true),
new Product("マウス", "PC", 2500, true),
new Product("モニター", "PC", 24000, false),
new Product("USB メモリ", "PC", 1800, true)
};
Take で先頭から取り出す
Take は、先頭から指定した件数だけ取り出します。
var firstThree = products.Take(3);
foreach (Product product in firstThree)
{
Console.WriteLine(product.Name);
}
実行結果です。
ノート
ペン
キーボード
検索結果の上位 10 件だけ表示したい場合などに使えます。
var top10 = products
.OrderBy(product => product.Price)
.Take(10);
Skip で先頭を飛ばす
Skip は、先頭から指定した件数を飛ばします。
var afterTwo = products.Skip(2);
foreach (Product product in afterTwo)
{
Console.WriteLine(product.Name);
}
実行結果です。
キーボード
マウス
モニター
USB メモリ
Skip と Take を組み合わせると、ページングを実装できます。
int page = 2;
int pageSize = 2;
var pageItems = products
.OrderBy(product => product.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize);
この例では、2 ページ目の 2 件を取得しています。
範囲を使ったページング
.NET 6 以降では、Take に範囲を渡す書き方も使えます。
var pageItems = products
.OrderBy(product => product.Name)
.Take(2..4);
これは、0 から数えて 2 番目以上、4 番目未満の要素を取り出すイメージです。
ただし、ページ番号とページサイズから計算する処理では、従来の Skip と Take の方が意図が分かりやすいことも多いです。
var pageItems = products
.Skip((page - 1) * pageSize)
.Take(pageSize);
チームで読みやすい方を選ぶとよいでしょう。
Chunk で一定サイズごとに分割する
.NET 6 以降では、Chunk を使ってコレクションを一定サイズごとに分割できます。
var chunks = products.Chunk(2);
int pageNumber = 1;
foreach (Product[] chunk in chunks)
{
Console.WriteLine($"ページ {pageNumber}");
foreach (Product product in chunk)
{
Console.WriteLine(product.Name);
}
pageNumber++;
}
実行結果です。
ページ 1
ノート
ペン
ページ 2
キーボード
マウス
ページ 3
モニター
USB メモリ
Chunk は、一覧をまとめて分割して処理したいときに便利です。
Count で件数を取得する
Count は、要素数を取得します。
int count = products.Count();
条件付きで数えることもできます。
int inStockCount = products.Count(product => product.InStock);
Console.WriteLine($"在庫あり: {inStockCount}件");
実行結果です。
在庫あり: 4件
条件に合う要素が存在するかだけを知りたい場合は、Count() > 0 より Any() が適しています。
bool hasExpensiveProduct = products.Any(product => product.Price >= 10000);
Any は、条件に合う要素を見つけた時点で処理を終えられます。
TryGetNonEnumeratedCount
.NET 6 以降では、TryGetNonEnumeratedCount を使うと、列挙せずに件数を取得できる場合があります。
IEnumerable<Product> query = products.Where(product => product.InStock);
if (query.TryGetNonEnumeratedCount(out int count))
{
Console.WriteLine($"列挙せずに件数を取得できました: {count}");
}
else
{
Console.WriteLine("列挙しないと件数を取得できません");
}
配列や List<T> のように件数をすでに持っているコレクションでは、列挙せずに件数を取得できることがあります。
一方、Where のような条件付きクエリでは、実際に調べないと件数が分からない場合があります。
普段から頻繁に使うメソッドではありませんが、IEnumerable<T> の件数取得には「列挙が必要な場合がある」という点を理解しておくとよいです。
Distinct で重複を取り除く
Distinct は、重複を取り除きます。
string[] categories = { "PC", "文房具", "PC", "書籍", "文房具" };
var distinctCategories = categories.Distinct();
foreach (string category in distinctCategories)
{
Console.WriteLine(category);
}
実行結果です。
PC
文房具
書籍
オブジェクトから特定のキーで重複を取り除きたい場合は、DistinctBy が使えます。
var categoryRepresentatives = products.DistinctBy(product => product.Category);
DistinctBy は .NET 6 以降で使えます。
集合演算
LINQ には、集合を扱うメソッドがあります。
string[] onlineUsers = { "Alice", "Bob", "Carol" };
string[] paidUsers = { "Bob", "Dave", "Alice" };
和集合は Union です。
var allUsers = onlineUsers.Union(paidUsers);
実行結果です。
Alice
Bob
Carol
Dave
積集合は Intersect です。
var onlineAndPaidUsers = onlineUsers.Intersect(paidUsers);
実行結果です。
Alice
Bob
差集合は Except です。
var onlineButNotPaidUsers = onlineUsers.Except(paidUsers);
実行結果です。
Carol
集合演算は、ベン図で考えると分かりやすいです。
| メソッド | 意味 |
|---|---|
Union |
どちらかに含まれる |
Intersect |
両方に含まれる |
Except |
左側にだけ含まれる |
Distinct |
重複を取り除く |
Sum で合計する
Sum は数値の合計を求めます。
int totalPrice = products.Sum(product => product.Price);
Console.WriteLine($"合計: {totalPrice}円");
実行結果です。
合計: 33150円
条件と組み合わせることもできます。
int inStockTotal = products
.Where(product => product.InStock)
.Sum(product => product.Price);
Average、Min、Max
平均は Average です。
double averagePrice = products.Average(product => product.Price);
最小値は Min、最大値は Max です。
int minPrice = products.Min(product => product.Price);
int maxPrice = products.Max(product => product.Price);
Console.WriteLine($"平均: {averagePrice:F0}円");
Console.WriteLine($"最小: {minPrice}円");
Console.WriteLine($"最大: {maxPrice}円");
MinBy や MaxBy を使うと、最小値や最大値を持つ要素そのものを取得できます。
Product cheapest = products.MinBy(product => product.Price)!;
Product mostExpensive = products.MaxBy(product => product.Price)!;
MinBy / MaxBy は .NET 6 以降で使えます。
Aggregate で任意の集計を行う
Aggregate は、要素を順番に処理して 1 つの値にまとめるメソッドです。
たとえば、商品名を > でつないで表示してみます。
string route = products
.Select(product => product.Name)
.Aggregate((current, next) => $"{current} > {next}");
Console.WriteLine(route);
実行結果です。
ノート > ペン > キーボード > マウス > モニター > USB メモリ
合計や平均のような代表的な集計には専用メソッドがあります。
Aggregate は、専用メソッドでは表しにくい独自の集計が必要なときに使います。
まとめ
LINQ to Objects には、データの一部を取り出したり、集合として扱ったり、集計したりするためのメソッドが用意されています。
Takeは先頭から指定件数を取り出すSkipは先頭から指定件数を飛ばすSkipとTakeを組み合わせるとページングを実装できるChunkは一定サイズごとに分割するCountは件数を数える- 存在確認だけなら
Anyが向いている Distinctは重複を取り除くUnion、Intersect、Exceptは集合演算を行うSum、Average、Min、Maxは代表的な集計処理を行うAggregateは任意の集計を行う
次回は、LINQ クエリが内部的にどのように表現されるのかを見ていきます。