はじめに
そもそも LINQ ってなに?
C# で配列やリストを扱うとき、こんなコードを書いたことはありませんか?
var result = new List<string>();
foreach (var user in users)
{
if (user.IsActive && user.Age > 20)
{
result.Add(user.Name);
}
}
実は、これと同じ処理をたった1行で書ける仕組みが「LINQ (Language Integrated Query)」です。
var result = users.Where(u => u.IsActive && u.Age > 20)
.Select(u => u.Name);
LINQ は、データに対して SQL のようなクエリを直接 C# の構文で書ける仕組みです。
なにがいいの?
| メリット | 詳細 |
|---|---|
| 可読性 | 「何をしたいか」が一目で分かる(foreach が長すぎると読むの大変ですよね) |
| 再利用性 | メソッドチェーンでフィルタやソートを組み合わせやすい |
| 一貫性 | 配列・コレクション・DB・XMLなど、同じ構文で扱える |
| 安全性 | 型安全にクエリを書ける(実行時エラーが減る) |
つまり、SQL の便利さを C# の中に持ち込んだものと考えると分かりやすいです。
メソッド構文 vs クエリ式構文
LINQ には2種類の書き方があります。
メソッド構文
var result = users.Where(u => u.IsActive)
.Select(u => u.Name);
クエリ構文
var result = from u in users
where u.IsActive
select u.Name;
どちらも同じ結果を返しますが、メソッド構文が主流になっているので、本記事ではメソッド構文を使用します。
IEnumerable / IQueryable の違い
LINQ を理解するうえで避けて通れないのが、この2つのインターフェイスの違いです。
扱うデータの場所と評価タイミングが異なります。
| 型 | 対象 | 実行タイミング | 主な用途 |
|---|---|---|---|
IEnumerable<T> |
メモリ上のデータ(List, Arrayなど) | その場で列挙(即時実行) | LINQ to Objects(コレクション操作) |
IQueryable<T> |
データベースなど外部ソース | クエリを式として翻訳し、後で実行(遅延実行) | LINQ to SQL / Entity Framework |
つまり、
-
IEnumerableは「すでにメモリ上にあるデータ」を操作します。
例:Listや配列など、C#アプリ内にロード済みのデータ。 -
IQueryableは「まだデータベースにあるデータ」を操作します。
LINQの式がSQLなどに翻訳され、サーバー側で処理されてから結果が返ってくるのが特徴です。
// IEnumerable:すでにメモリ上のデータを操作
var users = userList.Where(u => u.Age > 20);
// IQueryable:DB上のデータをフィルタ(SQLに変換される)
var users = dbContext.Users.Where(u => u.Age > 20);
両方同じに見えますが、
- IEnumerable は C# で実行
- IQueryable は SQL に翻訳されて実行
という違いがあります。
これを意識しておくと、LINQ のパフォーマンス最適化や実行タイミングの理解に大きく役立ちます。
LINQ の基本パターン
LINQ の使い方は大きく「フィルタ」、「変換」、「集計」に分けられます。
まずは代表的なメソッドをざっくり紹介します。
| 操作 | メソッド | 説明 |
|---|---|---|
| フィルタリング | Where |
条件に一致する要素を抽出 |
| 射影(変換) | Select |
必要な要素だけ取り出したり、形を変える |
| 並び替え |
OrderBy / ThenBy
|
昇順・降順に並べ替える |
| 集約 |
Count, Sum, Average, Aggregate
|
集計・統計処理 |
| 重複排除 | Distinct |
重複する要素を1つにまとめる |
| 平坦化 | SelectMany |
ネストしたコレクションをフラットにする |
フィルタリング(Where)
年齢が20歳以上のユーザーだけを取得します。
複数条件を組み合わせることも可能です。
var activeAdults = users.Where(u => u.IsActive && u.Age >= 20);
射影(Select)
ユーザーの名前だけを取得します。
var names = users.Select(u => u.Name);
また、匿名型で複数の情報をまとめることも可能です。
var info = users.Select(u => new { u.Name, u.Age });
並び替え(OrderBy / OrderByDescending / ThenBy / ThenByDescending)
年齢順、同年齢の場合は名前順に並び替えする。
昇順・降順もあります。
var sorted = users.OrderBy(u => u.Age)
.ThenBy(u => u.Name);
集約(Count / Sum / Average)
平均年齢を取得する。
var avg = users.Average(u => u.Age);
Sum や Count も同様に使用できます。
重複排除(Distinct)
カテゴリ名の重複を排除して、一意なリストを作成する。
var categories = products.Select(p => p.Category)
.Distinct();
平坦化(SelectMany)
各記事のタグリストをひとつのリストにまとめる
var tags = articles.SelectMany(a => a.Tags);
遅延実行と評価タイミングを理解する
LINQ の多くのメソッドは今すぐ実行されないという特徴があります。
これを一般的に「遅延実行」と言います。
遅延実行の基本
Whereで絞っただけでは、実はクエリは実行されていません。
var adults = users.Where(u => u.Age >= 20); // まだ実行されていない
foreach (var adult in adults)
{
Console.WriteLine(adult.Name); // ここで実行される
}
評価が繰り返されるケース
var activeUser = users.Where(u => u.IsActive);
var count = activeUser.Count(); // 1回目の実行
var list = activeUser.ToList(); // 2回目の実行(再評価されてしまう)
これはパフォーマンスを落とす原因になるので、一度ToList()で確定させてメモリ上で保持できる状態にします。
var activeUser = users.Where(u => u.IsActive).ToList();
var count = activeUser.Count;
遅れて発生する例外に注意!
var q = users.Select(u => 100 / u.Divisor); // ここで例外はスローされない
var arr = q.ToArray(); // ここで DivideByZeroException がスローされる
遅延実行中の例外は列挙されたタイミングでスローされます。
想定外の場所で落ちることがあるので注意が必要です。
ToList / ToArray で確定させるべきタイミング
- 同じクエリを複数回使うとき
- 例外を早めにスローさせたいとき
- using で閉じる DBContext を跨ぐとき
クライアント評価の罠
ToList()の位置が違うだけで、実行場所が変わります。
DB (サーバー)と、メモリ(ローカル)か以下のように切り分けることができます。
サーバーに負荷をかけたくない場合は、クライアント側でフィルタリングすればいいですし、クライアントの性能が低いならサーバー側でフィルタリングするといった風に場合によって切り分けましょう。
// クライアント側でフィルタ
var users = dbContext.Users.ToList().Where(u => u.Age > 30);
// サーバー側でフィルタ
var users = dbContext.Users.Where(u => u.Age > 30).ToList();
まとめ
本記事では、LINQ の基本構文と動作の仕組みを中心に紹介しました。
細かい最適化や内部実装は省きましたが、どこで実行され、いつ評価されるかを意識するだけで、LINQ を安全かつ効率的に使えるようになります。
まずは Where() や Select() など基本メソッドから試してみましょう。