前回は、LINQ to Objects のページング、集合演算、集計を見ました。
今回は、LINQ クエリが内部的にどのように表現されるのかを整理します。
LINQ のクエリ構文は、専用の魔法のように見えるかもしれません。
しかし、LINQ to Objects では、多くの場合、最終的に Enumerable クラスの拡張メソッド呼び出しとして扱われます。
クエリ構文はメソッド構文に変換される
まず、次のクエリ構文を見てみます。
int[] numbers = { 1, 2, 3, 4, 5, 6 };
var query =
from number in numbers
where number % 2 == 0
select number * 10;
これは、概念的には次のメソッド構文に変換されます。
var query = numbers
.Where(number => number % 2 == 0)
.Select(number => number * 10);
さらに、拡張メソッドを通常の静的メソッド呼び出しとして書くと、次のようになります。
var query = Enumerable.Select(
Enumerable.Where(numbers, number => number % 2 == 0),
number => number * 10
);
つまり、クエリ構文は読みやすい表記であり、裏側では Where や Select の呼び出しに対応しています。
Enumerable クラス
LINQ to Objects の多くのメソッドは、System.Linq.Enumerable クラスに定義されています。
using System.Linq;
Enumerable.Where のイメージは、次のようなメソッドです。
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
第 1 引数の source が対象のコレクションです。
第 2 引数の predicate が、要素を残すかどうかを判定する処理です。
this IEnumerable<TSource> source という形になっているため、拡張メソッドとして呼び出せます。
numbers.Where(number => number % 2 == 0)
通常の静的メソッドとして呼ぶなら、次のようになります。
Enumerable.Where(numbers, number => number % 2 == 0)
ラムダ式を使ったクエリ
LINQ のメソッド構文では、ラムダ式がよく使われます。
var result = numbers
.Where(number => number % 2 == 0)
.Select(number => number * 10);
Where に渡しているラムダ式は、次の形です。
number => number % 2 == 0
これは、int を受け取り、bool を返す処理です。
つまり、Func<int, bool> として扱えます。
Func<int, bool> isEven = number => number % 2 == 0;
Select に渡しているラムダ式は、次の形です。
number => number * 10
これは、int を受け取り、int を返す処理です。
Func<int, int> multiplyByTen = number => number * 10;
このように、LINQ の条件や変換は、デリゲートとして渡されています。
ラムダ式を変数に分ける
ラムダ式を変数に分けると、LINQ が何を受け取っているのかが見えやすくなります。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Func<int, bool> isEven = number => number % 2 == 0;
Func<int, int> multiplyByTen = number => number * 10;
IEnumerable<int> result = numbers
.Where(isEven)
.Select(multiplyByTen);
foreach (int number in result)
{
Console.WriteLine(number);
}
}
}
実行結果です。
20
40
60
Where は判定処理を受け取り、Select は変換処理を受け取っています。
LINQ は、処理そのものを引数として渡すことで柔軟な問い合わせを実現しています。
匿名メソッドで書く
ラムダ式の代わりに、匿名メソッドでも書けます。
var result = numbers
.Where(delegate (int number)
{
return number % 2 == 0;
})
.Select(delegate (int number)
{
return number * 10;
});
実行結果はラムダ式と同じです。
ラムダ式は、匿名メソッドをより簡潔に書ける構文だと考えられます。
LINQ では条件や変換を頻繁に書くため、ラムダ式の方が読みやすくなります。
生のデリゲートで書く
さらに古い書き方として、デリゲートオブジェクトを明示的に作ることもできます。
Func<int, bool> isEven = new Func<int, bool>(IsEven);
Func<int, int> multiplyByTen = new Func<int, int>(MultiplyByTen);
var result = numbers
.Where(isEven)
.Select(multiplyByTen);
static bool IsEven(int number)
{
return number % 2 == 0;
}
static int MultiplyByTen(int number)
{
return number * 10;
}
現在は、次のようにラムダ式で書くのが一般的です。
var result = numbers
.Where(number => number % 2 == 0)
.Select(number => number * 10);
しかし、内部的には「条件や変換の処理をデリゲートとして渡している」という点は変わりません。
クエリ構文とメソッド構文の対応
代表的なクエリ構文とメソッド構文の対応を見てみます。
var query =
from product in products
where product.InStock
orderby product.Price
select product.Name;
これは、概念的には次のように対応します。
var query = products
.Where(product => product.InStock)
.OrderBy(product => product.Price)
.Select(product => product.Name);
| クエリ構文 | メソッド構文 |
|---|---|
where |
Where |
orderby |
OrderBy / OrderByDescending |
select |
Select |
複数の from |
SelectMany |
group ... by ... |
GroupBy |
join |
Join |
すべてを完全に一対一で暗記する必要はありません。
まずは、クエリ構文はメソッド呼び出しに変換される、という感覚を持つことが大切です。
複数の from と SelectMany
クエリ構文で複数の from を使うと、入れ子のコレクションを平らに扱えます。
var query =
from order in orders
from item in order.Items
select item;
これは、概念的には SelectMany に対応します。
var query = orders.SelectMany(order => order.Items);
SelectMany は、コレクションの中にあるコレクションを 1 つの流れにします。
List<Order> orders = new List<Order>
{
new Order(1, new List<string> { "ノート", "ペン" }),
new Order(2, new List<string> { "マウス" })
};
var items =
from order in orders
from item in order.Items
select item;
foreach (string item in items)
{
Console.WriteLine(item);
}
実行結果です。
ノート
ペン
マウス
GroupBy の内部イメージ
GroupBy は、キーごとに要素をまとめます。
var groups = products.GroupBy(product => product.Category);
クエリ構文では、次のように書けます。
var groups =
from product in products
group product by product.Category;
GroupBy の結果は、キーと要素の集まりです。
foreach (var group in groups)
{
Console.WriteLine(group.Key);
foreach (Product product in group)
{
Console.WriteLine($" {product.Name}");
}
}
group.Key にはカテゴリ名が入り、group 自体はそのカテゴリに属する商品を列挙できます。
遅延実行は Enumerable の実装による
LINQ to Objects の多くのメソッドは、IEnumerable<T> を返します。
IEnumerable<int> query = numbers.Where(number => number % 2 == 0);
IEnumerable<T> は、要素を順番に取り出すためのインターフェースです。
そのため、Where を呼んだ時点では、結果のリストが作られているとは限りません。
実際に foreach などで列挙されたときに、必要な要素を順番に取り出します。
foreach (int number in query)
{
Console.WriteLine(number);
}
この遅延実行により、LINQ は複数の処理を効率よくつなげられます。
var query = numbers
.Where(number => number % 2 == 0)
.Select(number => number * 10)
.Take(2);
このような処理では、必要な分だけ要素を処理しながら結果を返せます。
即時実行メソッド
一方で、すぐに結果を確定するメソッドもあります。
List<int> list = query.ToList();
int count = query.Count();
int sum = query.Sum();
bool exists = query.Any();
これらは、結果を作るためにクエリを列挙します。
| 種類 | 例 |
|---|---|
| 列挙を返す | Where, Select, OrderBy, Take, Skip |
| 結果を確定する | ToList, ToArray, Count, Sum, Any, First |
LINQ を読むときは、「この時点で実行されるのか」「あとで列挙されたときに実行されるのか」を意識すると、挙動を理解しやすくなります。
実践例: クエリを組み立ててから実行する
遅延実行を利用すると、条件に応じてクエリを組み立ててから、最後に実行できます。
using System;
using System.Collections.Generic;
using System.Linq;
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;
}
}
class Program
{
static void Main()
{
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)
};
string? category = "PC";
int? maxPrice = 5000;
bool onlyInStock = true;
IEnumerable<Product> query = products;
if (!string.IsNullOrEmpty(category))
{
query = query.Where(product => product.Category == category);
}
if (maxPrice is not null)
{
query = query.Where(product => product.Price <= maxPrice.Value);
}
if (onlyInStock)
{
query = query.Where(product => product.InStock);
}
List<Product> result = query
.OrderBy(product => product.Price)
.ToList();
foreach (Product product in result)
{
Console.WriteLine($"{product.Name}: {product.Price}円");
}
}
}
実行結果です。
マウス: 2500円
キーボード: 4500円
query に条件を少しずつ追加し、最後に ToList() で実行しています。
検索画面や API の絞り込み処理でよく使う形です。
まとめ
LINQ to Objects のクエリは、内部的には Enumerable の拡張メソッド呼び出しとして考えられます。
- クエリ構文は、メソッド構文に変換される
Where、Select、OrderByなどはEnumerableの拡張メソッドである- LINQ の条件や変換は、ラムダ式として渡される
- ラムダ式は
Func<T, TResult>などのデリゲートとして扱われる - ラムダ式の代わりに匿名メソッドや明示的なデリゲートでも書ける
- 複数の
fromはSelectManyに対応する group ... by ...はGroupByに対応する- 多くの LINQ メソッドは遅延実行される
ToList、Count、Sum、Anyなどは結果を確定する
LINQ は便利な構文ですが、根底には IEnumerable<T>、拡張メソッド、ラムダ式、デリゲートがあります。
これまで見てきた C# の機能がつながると、LINQ は単なる便利メソッドの集まりではなく、C# らしいデータ処理の書き方として理解できるようになります。