【C#】LINQ完全ガイド|Where・Select・SelectMany・First・Any・Take・Distinct・遅延評価まで

LINQ(Language Integrated Query)は C# でコレクション・配列・データベースなどを統一的に操作できる仕組みです。基本の WhereSelect を知っているだけでは、実務でよく使う SelectManyFirstOrDefaultAnyDistinct などが書けません。

本記事では LINQ の遅延評価という核心概念から始めて、よく使うメソッドをすべて網羅します。ソート・グルーピングはLINQの応用(GroupBy・OrderBy・Join)を参照してください。

スポンサーリンク

LINQ の仕組み:遅延評価とパイプライン

LINQ を理解する上で最も重要な概念が遅延評価(Deferred Execution)です。LINQ のほとんどのメソッドは「クエリを組み立てた時点では何も実行しない」という性質を持っています。

遅延評価の動作確認
int[] numbers = { 1, 2, 3, 4, 5 };

// この時点ではまだ何も実行されない(クエリの組み立てのみ)
var query = numbers.Where(n =>
{
    Console.WriteLine($"  評価中: {n}"); // ← いつ動く?
    return n % 2 == 0;
});

Console.WriteLine("foreach 開始前");
foreach (var n in query)           // ← ここで初めて評価が走る
    Console.WriteLine($"結果: {n}");

// 出力:
// foreach 開始前
//   評価中: 1
//   評価中: 2   結果: 2
//   評価中: 3
//   評価中: 4   結果: 4
//   評価中: 5
遅延評価のメリット: 全要素を処理してから返すのではなく、必要になるまで評価を先送りするため、100万件のリストに対して Where(...).Take(5) とすると最初の5件が見つかった時点で処理を止めます。全件処理する for ループより大幅に高速になる場合があります。

即時実行メソッドで評価を確定させる

ToList / ToArray / ToDictionary で即時実行
// ToList() を呼んだ時点で評価が実行され、結果がリストに格納される
List<int> result = numbers.Where(n => n % 2 == 0).ToList();

// 複数回ループするときは ToList() して変数に入れておく
// (そうしないと foreach するたびに Where が再実行される)
var expensive = numbers
    .Where(n => { /* 重い処理 */ return n > 2; })
    .ToList(); // ← 1度だけ評価

foreach (var n in expensive) Console.WriteLine(n); // リストを参照するだけ
foreach (var n in expensive) Console.WriteLine(n); // 同上(Where は再実行されない)
メソッド 実行タイミング 戻り値型 用途
ToList() 即時実行 List<T> 最も一般的。結果をキャッシュしたいとき
ToArray() 即時実行 T[] 固定サイズの配列が欲しいとき
ToDictionary() 即時実行 Dictionary<K,V> キーで引ける辞書に変換
ToHashSet() 即時実行 HashSet<T> 重複除去・高速検索が必要なとき
ToLookup() 即時実行 ILookup<K,V> キーに複数値が対応する辞書
Where / Select など 遅延実行 IEnumerable<T> 列挙されるまで実行されない

メソッド構文とクエリ構文

LINQ には 2 種類の書き方があります。どちらも同じ結果を返しますが、実務ではメソッド構文が主流です。

メソッド構文 vs クエリ構文
var products = new[]
{
    new { Name = "りんご", Category = "果物", Price = 150 },
    new { Name = "みかん", Category = "果物", Price = 100 },
    new { Name = "牛乳",   Category = "乳製品", Price = 180 },
    new { Name = "チーズ", Category = "乳製品", Price = 300 },
};

// ─── メソッド構文(Lambda 式を連鎖)─────────────
var method = products
    .Where(p => p.Category == "果物")
    .Select(p => p.Name);

// ─── クエリ構文(SQL に近い書き方)───────────────
var query = from p in products
            where p.Category == "果物"
            select p.Name;

// 結果は同じ: ["りんご", "みかん"]
Console.WriteLine(string.Join(", ", method));
Console.WriteLine(string.Join(", ", query));
クエリ構文はコンパイラによってメソッド構文に変換されます。GroupBySelect を組み合わせる場合はクエリ構文の方が読みやすいこともありますが、SelectManyDistinctZip などはメソッド構文でしか書けないため、メソッド構文を習得しておくと実務ですべての状況に対応できます。

Where:条件でフィルタリングする

Where の使い方
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// ─── 基本的なフィルタリング ──────────────────────
var even      = numbers.Where(n => n % 2 == 0);          // 偶数
var over5     = numbers.Where(n => n > 5);                // 5より大きい
var evenOver5 = numbers.Where(n => n % 2 == 0 && n > 5); // 偶数かつ5超

// ─── オブジェクトのプロパティでフィルタリング ────
var expensiveProducts = products.Where(p => p.Price >= 200);

// ─── 文字列のフィルタリング ──────────────────────
string[] names = { "Alice", "Bob", "Charlie", "Anna" };
var aNames  = names.Where(n => n.StartsWith("A"));   // A で始まる
var long5   = names.Where(n => n.Length >= 5);       // 5文字以上
var hasO    = names.Where(n => n.Contains("o", StringComparison.OrdinalIgnoreCase));

// ─── インデックスを使ったフィルタリング(オーバーロード)
var atEvenIndex = numbers.Where((n, i) => i % 2 == 0); // 偶数インデックスの要素

Select:要素を変換・射影する

Select の使い方
var numbers = new[] { 1, 2, 3, 4, 5 };

// ─── 基本的な変換 ────────────────────────────────
var doubled  = numbers.Select(n => n * 2);            // 各要素を2倍
var asString = numbers.Select(n => n.ToString());     // int → string

// ─── プロパティの取り出し ─────────────────────────
var names  = products.Select(p => p.Name);
var prices = products.Select(p => p.Price);

// ─── 匿名型で複数プロパティを組み合わせる ────────
var summary = products.Select(p => new
{
    p.Name,
    p.Category,
    PriceWithTax = p.Price * 1.1
});

foreach (var item in summary)
    Console.WriteLine($"{item.Name}: {item.PriceWithTax:F0}円(税込)");

// ─── インデックス付きの Select ────────────────────
var indexed = products.Select((p, i) => $"{i + 1}. {p.Name}");
// "1. りんご", "2. みかん", ...

// ─── record や DTO への変換(実務でよく使う)─────
record ProductDto(string Name, int Price);
var dtos = products.Select(p => new ProductDto(p.Name, p.Price));

SelectMany:コレクションをフラット化する

SelectMany はリストのリストを1つのリストに平坦化します。Select と間違えやすいので違いを押さえましょう。

Select vs SelectMany の違い
var orders = new[]
{
    new { Customer = "田中", Items = new[] { "りんご", "みかん" } },
    new { Customer = "鈴木", Items = new[] { "バナナ" } },
    new { Customer = "佐藤", Items = new[] { "ぶどう", "もも", "梨" } },
};

// ─── Select: コレクションのコレクションになる ─────
var nested = orders.Select(o => o.Items);
// IEnumerable<string[]> → 3つの配列のシーケンス

// ─── SelectMany: 1つのシーケンスに平坦化 ─────────
var flat = orders.SelectMany(o => o.Items);
// IEnumerable<string> → "りんご","みかん","バナナ","ぶどう","もも","梨"

Console.WriteLine(string.Join(", ", flat));

// ─── SelectMany + 結果セレクタ(元のオブジェクトと組み合わせ)
var withCustomer = orders.SelectMany(
    o => o.Items,
    (order, item) => $"{order.Customer}: {item}"
);
// "田中: りんご", "田中: みかん", "鈴木: バナナ", ...

// ─── ネストした LINQ でも同様 ─────────────────────
// 文字列リストの各文字を全部展開
string[] words = { "abc", "de" };
var chars = words.SelectMany(w => w); // 'a','b','c','d','e'

要素の取得:First・Single・Last・ElementAt

1件の要素を取得するメソッド群
var numbers = new[] { 1, 2, 3, 4, 5 };
var empty   = Array.Empty<int>();

// ─── First / Last ─────────────────────────────────
int first = numbers.First();          // 1(空なら例外)
int last  = numbers.Last();           // 5(空なら例外)
int firstEven = numbers.First(n => n % 2 == 0); // 2

// ─── FirstOrDefault / LastOrDefault ──────────────
int fd  = empty.FirstOrDefault();         // 0(デフォルト値)
int fd2 = empty.FirstOrDefault(-1);       // -1(.NET 6+: 既定値を指定)
int fd3 = numbers.FirstOrDefault(n => n > 10); // 0(見つからない)

// ─── Single / SingleOrDefault ─────────────────────
// Single: 1件のみのはず → 0件や2件以上は例外を投げる
int only = numbers.Single(n => n == 3);   // 3
// numbers.Single(n => n % 2 == 0); → 例外(偶数が複数ある)

// SingleOrDefault: 0件は OK(デフォルト値)、2件以上は例外
int sod = numbers.SingleOrDefault(n => n > 10); // 0

// ─── ElementAt ────────────────────────────────────
int atIndex2 = numbers.ElementAt(2);           // 3(0始まり)
int atOr     = numbers.ElementAtOrDefault(10); // 0(範囲外)
メソッド 動作 使いどき
First() 先頭要素(空なら例外) 要素が必ずあると確信できるとき
FirstOrDefault() 先頭要素 or デフォルト値 0件の可能性があるとき(最も安全)
Single() 唯一の要素(0件/2件以上で例外) ID検索など一意性を保証したいとき
SingleOrDefault() 唯一の要素 or デフォルト値 0件は許容、2件以上は設計エラーなとき
Last() 末尾要素(空なら例外) 末尾を取得(コスト大: 全件走査)
ElementAt(i) i 番目の要素(範囲外で例外) インデックスで特定要素を取得

Any・All・Contains:条件の検査

Any・All・Contains の使い方
var numbers = new[] { 1, 2, 3, 4, 5 };

// ─── Any: 条件を満たす要素が1件でもあるか ─────────
bool hasEven    = numbers.Any(n => n % 2 == 0);  // true
bool hasOver10  = numbers.Any(n => n > 10);       // false
bool isNotEmpty = numbers.Any();                   // true(引数なしで空チェック)

// ─── All: すべての要素が条件を満たすか ────────────
bool allPositive = numbers.All(n => n > 0);  // true
bool allOver3    = numbers.All(n => n > 3);  // false

// ─── Contains: 特定の値が含まれるか ──────────────
bool has3     = numbers.Contains(3);  // true
bool has99    = numbers.Contains(99); // false

// 文字列の Contains(大文字小文字を無視)
string[] names = { "Alice", "Bob", "Charlie" };
bool hasBob = names.Contains("bob", StringComparer.OrdinalIgnoreCase); // true

// ─── 実践例: 空チェックのベストプラクティス ──────
IEnumerable<int> data = GetData();
if (!data.Any())
    Console.WriteLine("データがありません");
// NG: data.Count() == 0 → 全件走査してしまう
// OK: !data.Any()       → 1件見つかった時点で止まる
コレクションが空かどうかの確認には Count() == 0 ではなく !Any() を使いましょう。Count() は全件を走査するのに対し、Any() は最初の要素が見つかった時点で true を返すため、大きなシーケンスほど差が出ます。

集計メソッド:Count・Sum・Min・Max・Average

集計メソッドの使い方
var prices = new[] { 100, 200, 150, 300, 250 };

// ─── 基本集計 ─────────────────────────────────────
int count   = prices.Count();              // 5(全件数)
int condCnt = prices.Count(p => p >= 200); // 3(条件付き件数)
int sum     = prices.Sum();                // 1000
int min     = prices.Min();                // 100
int max     = prices.Max();                // 300
double avg  = prices.Average();            // 200.0

// ─── オブジェクトのプロパティで集計 ─────────────
var totalPrice = products.Sum(p => p.Price);
var maxPrice   = products.Max(p => p.Price);
var avgPrice   = products.Average(p => p.Price);

// ─── MinBy / MaxBy (.NET 6+):最小/最大要素を返す
var cheapest  = products.MinBy(p => p.Price); // 最安値の商品オブジェクト
var mostExp   = products.MaxBy(p => p.Price); // 最高値の商品オブジェクト
Console.WriteLine(cheapest?.Name); // "みかん"

// 旧来の方法(.NET 5 以前)
var cheapestOld = products.OrderBy(p => p.Price).First();

// ─── LongCount:int を超える件数のとき ─────────
long longCount = prices.LongCount();

Take・Skip:ページネーションと部分取得

Take・Skip・TakeWhile・SkipWhile の使い方
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// ─── Take: 先頭 N 件を取る ────────────────────────
var top3 = numbers.Take(3);          // 1, 2, 3

// ─── Skip: 先頭 N 件を飛ばして残りを取る ──────────
var skip3 = numbers.Skip(3);         // 4, 5, 6, 7, 8, 9, 10

// ─── ページネーション(定番パターン)─────────────
int page = 2;
int pageSize = 3;
var paged = numbers
    .Skip((page - 1) * pageSize)    // 3件スキップ
    .Take(pageSize);                // 3件取得 → 4, 5, 6

// ─── TakeLast / SkipLast (.NET Core 2.0+) ────────
var last3   = numbers.TakeLast(3);  // 8, 9, 10
var except2 = numbers.SkipLast(2); // 1, 2, 3, 4, 5, 6, 7, 8

// ─── TakeWhile: 条件を満たす間だけ取る ──────────
var whileLess5 = numbers.TakeWhile(n => n < 5); // 1, 2, 3, 4(5で打ち切り)

// ─── SkipWhile: 条件を満たす間はスキップ ─────────
var skipWhileLess5 = numbers.SkipWhile(n => n < 5); // 5, 6, 7, 8, 9, 10

// ─── Take + Range (.NET 6+) ───────────────────────
var range = numbers.Take(2..5); // 3, 4, 5(インデックス 2〜4)

重複除去と集合演算:Distinct・Except・Intersect・Union

Distinct・集合演算の使い方
var a = new[] { 1, 2, 3, 4, 5 };
var b = new[] { 3, 4, 5, 6, 7 };

// ─── Distinct: 重複を除去 ────────────────────────
var nums = new[] { 1, 2, 2, 3, 3, 3, 4 };
var unique = nums.Distinct(); // 1, 2, 3, 4

// ─── DistinctBy (.NET 6+):プロパティで重複除去 ──
var distinctProducts = products.DistinctBy(p => p.Category);
// カテゴリが重複する商品を除いた最初の1件ずつを返す

// ─── 集合演算 ─────────────────────────────────────
var union     = a.Union(b);     // 和集合:     1, 2, 3, 4, 5, 6, 7
var intersect = a.Intersect(b); // 積集合:     3, 4, 5
var except    = a.Except(b);    // 差集合:     1, 2

Console.WriteLine($"和集合:  {string.Join(",", union)}");
Console.WriteLine($"積集合:  {string.Join(",", intersect)}");
Console.WriteLine($"差集合:  {string.Join(",", except)}");

// ─── UnionBy / IntersectBy / ExceptBy (.NET 6+) ──
var set1 = products.Take(2);
var set2 = products.TakeLast(2);
var unionByName = set1.UnionBy(set2, p => p.Name);

Zip・Aggregate:応用メソッド

Zip と Aggregate の使い方
// ─── Zip: 2つのシーケンスを対応する要素でペア化 ──
var names  = new[] { "田中", "鈴木", "佐藤" };
var scores = new[] { 85, 92, 78 };

var combined = names.Zip(scores, (name, score) => $"{name}: {score}点");
foreach (var item in combined)
    Console.WriteLine(item); // "田中: 85点", "鈴木: 92点", "佐藤: 78点"

// .NET 6+: タプルで返す Zip
var tuples = names.Zip(scores); // IEnumerable<(string, int)>

// 3つのシーケンスを同時に Zip
var ids   = new[] { 1, 2, 3 };
var triple = names.Zip(scores, ids)
                  .Select(t => $"[{t.Third}]{t.First}:{t.Second}点");

// ─── Aggregate: 累積処理(LINQ の Reduce)────────
// 数値の積(1×2×3×4×5 = 120)
int product = new[] { 1, 2, 3, 4, 5 }.Aggregate((acc, n) => acc * n);
Console.WriteLine(product); // 120

// 初期値ありの Aggregate
string csv = new[] { "A", "B", "C" }
    .Aggregate("", (acc, s) => acc == "" ? s : $"{acc},{s}");
Console.WriteLine(csv); // "A,B,C"

// 集計 + 変換の同時実行
double average = new[] { 80, 90, 70 }.Aggregate(
    seed: (sum: 0, count: 0),
    func: (acc, n) => (acc.sum + n, acc.count + 1),
    resultSelector: acc => (double)acc.sum / acc.count
);
Console.WriteLine(average); // 80.0

ToDictionary・ToLookup:辞書への変換

ToDictionary と ToLookup の使い方
// ─── ToDictionary: キーが一意の場合 ──────────────
var productDict = products.ToDictionary(p => p.Name, p => p.Price);
Console.WriteLine(productDict["りんご"]); // 150

// ID をキーにして商品オブジェクトを引く
record Product2(int Id, string Name, string Category, int Price);
var byId = someProducts.ToDictionary(p => p.Id);
var found = byId[42]; // O(1) で検索

// ─── ToLookup: キーが重複する場合 ────────────────
// グループ化したまま辞書として使いたいとき
var byCategory = products.ToLookup(p => p.Category);

// カテゴリを指定してアクセス
foreach (var p in byCategory["果物"])
    Console.WriteLine(p.Name); // "りんご", "みかん"

// 存在しないキーでも空のシーケンスが返る(例外が出ない)
var nothing = byCategory["非食品"]; // 空のシーケンス
ToDictionary はキーの重複があると ArgumentException を投げます。同じキーに複数の値を対応させたい場合は ToLookup を使いましょう。Dictionary<K, List<V>> を自分でGroupByで作るより簡潔に書けます。

Concat・Append・Prepend:シーケンスの結合と追加

Concat・Append・Prepend の使い方
var first  = new[] { 1, 2, 3 };
var second = new[] { 4, 5, 6 };

// ─── Concat: 2つのシーケンスを結合 ───────────────
var merged = first.Concat(second); // 1, 2, 3, 4, 5, 6

// ─── Append: 末尾に1要素追加 ─────────────────────
var appended = first.Append(99); // 1, 2, 3, 99

// ─── Prepend: 先頭に1要素追加 ────────────────────
var prepended = first.Prepend(0); // 0, 1, 2, 3

// ─── 実践例: ヘッダー行付きで出力 ────────────────
var header = new[] { "名前", "価格" };
var rows   = products.Select(p => $"{p.Name}, {p.Price}");
foreach (var line in header.Concat(rows))
    Console.WriteLine(line);

実践例

商品リストの検索・集計・変換

LINQ を連鎖させた実践的な商品処理
record Product(string Name, string Category, int Price, bool InStock);

var catalog = new List<Product>
{
    new("りんご",   "果物",   150, true),
    new("みかん",   "果物",   100, false),
    new("バナナ",   "果物",   200, true),
    new("牛乳",     "乳製品", 180, true),
    new("チーズ",   "乳製品", 350, true),
    new("ヨーグルト","乳製品", 120, false),
};

// ─── 在庫ありの果物を価格昇順で表示 ──────────────
var available = catalog
    .Where(p => p.InStock && p.Category == "果物")
    .OrderBy(p => p.Price)
    .Select(p => $"{p.Name} {p.Price}円");

Console.WriteLine("在庫あり果物:");
foreach (var item in available) Console.WriteLine($"  {item}");

// ─── カテゴリ別の平均価格と最安商品 ──────────────
var categorySummary = catalog
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category = g.Key,
        Count    = g.Count(),
        AvgPrice = (int)g.Average(p => p.Price),
        Cheapest = g.MinBy(p => p.Price)!.Name,
    });

foreach (var s in categorySummary)
    Console.WriteLine($"{s.Category}: {s.Count}件, 平均{s.AvgPrice}円, 最安:{s.Cheapest}");

// ─── 在庫なし商品名を一覧化 ──────────────────────
string outOfStock = string.Join("・", catalog
    .Where(p => !p.InStock)
    .Select(p => p.Name));
Console.WriteLine($"在庫切れ: {outOfStock}");

テキスト処理:ログ行の解析

ログファイルのエラー行を抽出・集計する
string[] logLines =
{
    "[INFO]  2025-08-28 09:00 起動",
    "[ERROR] 2025-08-28 09:05 DB接続失敗",
    "[WARN]  2025-08-28 09:10 レスポンス遅延",
    "[ERROR] 2025-08-28 09:15 タイムアウト",
    "[INFO]  2025-08-28 09:20 正常処理",
    "[ERROR] 2025-08-28 09:25 ファイルが見つかりません",
};

// ─── ERROR 行だけ抽出 ─────────────────────────────
var errors = logLines
    .Where(line => line.StartsWith("[ERROR]"))
    .Select(line => line.Substring("[ERROR] ".Length))
    .ToList();

Console.WriteLine($"エラー数: {errors.Count}");
foreach (var e in errors) Console.WriteLine($"  {e}");

// ─── レベル別の件数集計 ──────────────────────────
var levelCounts = logLines
    .Select(line => line.Split(']')[0].TrimStart('['))
    .GroupBy(level => level)
    .Select(g => new { Level = g.Key, Count = g.Count() });

foreach (var l in levelCounts)
    Console.WriteLine($"{l.Level}: {l.Count}件");

よくある落とし穴と注意点

遅延評価によるコレクション変更バグ

遅延評価で変更が反映されるバグ
var list = new List<int> { 1, 2, 3, 4, 5 };

// クエリを組み立てた後にリストを変更する
var query = list.Where(n => n % 2 == 0); // まだ評価されていない

list.Add(6); // ← foreach の前にリストを変更

// foreach 時点で評価されるため 6 も含まれる
foreach (var n in query)
    Console.WriteLine(n); // 2, 4, 6 ← 6 が含まれる!

// 固定したいなら ToList() でスナップショットを取る
var snapshot = list.Where(n => n % 2 == 0).ToList(); // 変更前の時点で確定
list.Add(8);
foreach (var n in snapshot)
    Console.WriteLine(n); // 2, 4, 6(8は含まれない)

複数列挙で Where が何度も実行される

IEnumerable を複数回列挙する非効率なコード
// BAD: Where が2回実行される(重い処理なら2倍のコスト)
var filtered = items.Where(i => HeavyProcess(i));
Console.WriteLine(filtered.Count()); // 1回目の列挙
foreach (var item in filtered)       // 2回目の列挙
    Process(item);

// GOOD: ToList() で結果を固定してから使う
var filteredList = items.Where(i => HeavyProcess(i)).ToList(); // 1回だけ
Console.WriteLine(filteredList.Count);  // リストのプロパティ(再列挙なし)
foreach (var item in filteredList)      // リストを参照するだけ
    Process(item);

FirstOrDefault の戻り値が null になる

FirstOrDefault() は要素が見つからない場合に参照型は null、値型は 0 などのデフォルト値を返します。参照型に対して戻り値を null チェックなしで使うと NullReferenceException が発生します。.NET 6 以降では FirstOrDefault(defaultValue) でデフォルト値を指定できます。

よくある質問

QLINQ は for ループより遅いですか?
A実測すると単純なループとほぼ同等か、遅延評価が活きるケース(TakeFirst など)では LINQ の方が速いこともあります。ただし、ボックス化が発生するケースや IQueryable の変換コストが掛かる場面では差が出ることもあります。ボトルネックになるまで可読性優先で LINQ を使い、プロファイラで遅いと判明してからループに書き直すのが実務の判断基準です。
Qメソッド構文とクエリ構文はどちらを使うべきですか?
A実務ではメソッド構文が主流です。クエリ構文は GroupBy + intojoin ... on ... equals などが SQL に近く読みやすい場面がありますが、SelectManyDistinctSkipTake などはクエリ構文で書けないためメソッド構文を覚えれば十分です。チームで統一するなら迷わずメソッド構文を選びましょう。
QLINQ のパフォーマンスを改善するには?
AToList() で複数回の列挙を防ぐ ② Count() == 0 の代わりに !Any() を使う ③ データベースアクセスには IQueryable を使って DB 側にフィルタを移す ④ 不要な ToList() を挟まず遅延評価のまま連鎖させる の 4 点が基本です。詳しくはIEnumerable と IQueryable の違いを参照してください。
QWhere で null をフィルタしたいのですがどう書きますか?
Acollection.Where(x => x != null) または .NET 6 以降では collection.OfType<T>() を使うと null 要素を除いたシーケンスが得られます。OfType<T>() は型が一致しない要素も除けるため、IEnumerable<object> から特定の型の要素だけ取り出す場合にも使えます。
QLINQ で例外が発生した場合、どの要素で発生したかわかりますか?
AWhereSelect などの遅延評価メソッド内で例外が発生すると、スタックトレースには「LINQ の内部メソッド」のフレームが並び、どのインデックスの要素で発生したかが分かりにくいことがあります。デバッグ時は .ToList() で強制実行してから確認するか、インデックス付きの Select((item, i) => (item, i)) でインデックスを保持しながら処理すると特定しやすくなります。

まとめ

メソッド 用途
Where 条件でフィルタリング
Select 各要素を変換・射影
SelectMany コレクションのコレクションを平坦化
First / FirstOrDefault 先頭要素を取得(OrDefault は安全版)
Single / SingleOrDefault 唯一の要素を取得(2件以上で例外)
Any / All / Contains 条件の検査・存在確認
Count / Sum / Min / Max / Average 集計
MinBy / MaxBy (.NET 6+) 最小/最大要素のオブジェクトを返す
Take / Skip 先頭 N 件取得・スキップ(ページネーション)
TakeWhile / SkipWhile 条件を満たす間だけ取得・スキップ
Distinct / DistinctBy 重複除去
Union / Intersect / Except 集合演算
Concat / Append / Prepend シーケンスの結合・追加
Zip 2つのシーケンスを要素ごとにペア化
Aggregate 累積処理(Reduce)
ToList / ToArray / ToDictionary / ToHashSet 即時実行・コレクション変換
ToLookup キー重複ありの辞書に変換

ソート・グルーピングはLINQの応用(GroupBy・OrderBy・Join)、データベースとの連携はIEnumerableとIQueryableの違いも合わせて参照してください。ラムダ式の書き方はラムダ式と匿名メソッドの違いと使い分けで詳しく解説しています。