- 技術 💻
C# LINQの使い方一覧|Where・Select・GroupByを実行結果つきで
LINQ(リンク)とは、配列やリストなどのデータの集まりに対して、絞り込み・変換・並べ替え・集計を同じ書き方で行える C# の機能です。 products.Where(p => p.Price >= 1000).Select(p => p.Name) のように、メソッドをつなげて「何を取り出したいか」を書きます。for 文で1件ずつ処理するコードを、短く読みやすい形にできます。
この記事では、LINQ のメソッドを用途別(絞り込み・変換・並べ替え・集計・グループ化・結合・集合・要素の取得・判定・生成)の早見表にまとめ、主要なメソッドを .NET 8 で実行した結果を載せています。あわせて、クエリ構文とメソッド構文の対応、遅延評価で結果が変わる例、First と Single の例外メッセージ、Chunk・DistinctBy・MaxBy・Order など .NET 6 以降に追加されたメソッドの対応バージョン表まで扱います。実行環境は .NET SDK 8.0.131(ランタイム 8.0.31)です。
LINQメソッドの用途別早見表
| 用途 | メソッド | 働き |
|---|---|---|
| 絞り込み | Where |
条件に合う要素だけを残す |
| 絞り込み | Take / Skip / TakeLast / SkipLast |
先頭・末尾から指定した数を取る/飛ばす |
| 絞り込み | TakeWhile / SkipWhile |
条件を満たす間だけ取る/飛ばす |
| 絞り込み | Distinct / DistinctBy |
重複を取り除く(DistinctBy はキーで判定) |
| 絞り込み | OfType<T> |
指定した型の要素だけを残す |
| 変換 | Select |
各要素を別の形に変える |
| 変換 | SelectMany |
入れ子の集まりを1本に平らにする |
| 変換 | Chunk |
指定した数ごとに分割する |
| 変換 | Zip |
2つの集まりを同じ位置どうしで組み合わせる |
| 変換 | Cast<T> |
各要素を指定した型にキャストする |
| 並べ替え | OrderBy / OrderByDescending |
キーで昇順/降順に並べる |
| 並べ替え | ThenBy / ThenByDescending |
2つ目以降のキーで並べる |
| 並べ替え | Order / OrderDescending |
要素そのものの値で並べる |
| 並べ替え | Reverse |
逆順にする |
| 集計 | Count / Sum / Average / Min / Max |
件数・合計・平均・最小・最大 |
| 集計 | MinBy / MaxBy |
キーが最小/最大の要素そのものを返す |
| 集計 | Aggregate |
自分で決めた計算で1つの値にまとめる |
| グループ化 | GroupBy |
キーごとにグループに分ける |
| グループ化 | ToLookup |
キーで引ける形にまとめる(すぐに実行される) |
| 結合 | Join |
キーが一致する要素どうしを組み合わせる(内部結合) |
| 結合 | GroupJoin |
左側の各要素に、一致する右側の要素の集まりを付ける |
| 結合 | Concat |
2つの集まりを後ろにつなげる |
| 集合 | Union / Intersect / Except |
和集合・共通部分・差 |
| 集合 | UnionBy / IntersectBy / ExceptBy |
キーで判定する集合演算 |
| 要素の取得 | First / Last / Single |
最初・最後・ただ1つの要素(ないと例外) |
| 要素の取得 | FirstOrDefault / LastOrDefault / SingleOrDefault |
ないときは既定値を返す |
| 要素の取得 | ElementAt / ElementAtOrDefault |
指定した位置の要素 |
| 要素の取得 | DefaultIfEmpty |
空のときに既定値1件の集まりにする |
| 判定 | Any / All |
1つでも満たすか/すべて満たすか |
| 判定 | Contains / SequenceEqual |
含むか/並びが同じか |
| 生成 | Enumerable.Range / Repeat / Empty |
連番・繰り返し・空の集まりを作る |
| 生成 | Append / Prepend |
末尾/先頭に1件足す |
| 確定 | ToList / ToArray / ToDictionary / ToHashSet |
その場で実行して、コレクションにする |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C# LINQの使い方一覧|Where・Select・GroupByを実行結果つきで」
この記事のコードの動かし方と共通データ
コードは、dotnet new console -o linqsample で作ったプロジェクトの Program.cs に貼り、dotnet run で実行できます。.NET 6 以降のコンソールアプリでは System.Linq の using が自動で入りますが、ここではわかりやすいように明示しています。以降のコード片は、次の共通部分に続けて書く前提です(record の定義は Program.cs の末尾に置きます)。
using System;
using System.Collections.Generic;
using System.Linq;
var products = new List<Product>
{
new(1, "ノート", "文具", 180, 40),
new(2, "ボールペン", "文具", 120, 0),
new(3, "マウス", "PC", 1500, 12),
new(4, "キーボード", "PC", 4800, 5),
new(5, "椅子", "家具", 12000, 3),
new(6, "机", "家具", 28000, 0),
};
var orders = new List<Order>
{
new(101, 1, 10), new(102, 3, 2), new(103, 1, 5), new(104, 5, 1), new(105, 9, 1),
};
int[] nums = { 5, 3, 8, 3, 1, 8, 2 };
// 結果を1行で表示するための補助メソッド
void P<T>(string label, IEnumerable<T> seq) => Console.WriteLine($"{label,-28}=> {string.Join(", ", seq)}");
void V(string label, object? value) => Console.WriteLine($"{label,-28}=> {value}");
// (ここに各章のコードを書く)
record Product(int Id, string Name, string Category, int Price, int Stock);
record Order(int Id, int ProductId, int Qty);
products は商品(Id・名前・分類・価格・在庫数)、orders は注文(Id・商品Id・数量)です。注文105は、存在しない商品Id(9)を指しています。
絞り込む(Where・Take・Skip・Distinct)
P("Where(Price>=1000)", products.Where(p => p.Price >= 1000).Select(p => p.Name));
P("Where((p,i)=>i%2==0)", products.Where((p, i) => i % 2 == 0).Select(p => p.Name));
P("Take(2)", nums.Take(2));
P("Skip(5)", nums.Skip(5));
P("Take(2..5)", nums.Take(2..5));
P("TakeLast(2)", nums.TakeLast(2));
P("SkipLast(5)", nums.SkipLast(5));
P("TakeWhile(n=>n>2)", nums.TakeWhile(n => n > 2));
P("SkipWhile(n=>n>2)", nums.SkipWhile(n => n > 2));
P("Distinct()", nums.Distinct());
P("DistinctBy(Category)", products.DistinctBy(p => p.Category).Select(p => p.Name));
P("OfType<int>()", new object[] { 1, "a", 2, null!, 3.5 }.OfType<int>());
実行結果(.NET 8):
Where(Price>=1000) => マウス, キーボード, 椅子, 机
Where((p,i)=>i%2==0) => ノート, マウス, 椅子
Take(2) => 5, 3
Skip(5) => 8, 2
Take(2..5) => 8, 3, 1
TakeLast(2) => 8, 2
SkipLast(5) => 5, 3
TakeWhile(n=>n>2) => 5, 3, 8, 3
SkipWhile(n=>n>2) => 1, 8, 2
Distinct() => 5, 3, 8, 1, 2
DistinctBy(Category) => ノート, マウス, 椅子
OfType<int>() => 1, 2
Whereは条件がtrueの要素だけを残します。 2つ目の書き方のように、要素の位置(0始まり)も受け取れます。Skip(n).Take(m)を組み合わせると、ページ分け(n件飛ばしてm件取る)になります。Take(2..5)は範囲で指定する書き方で、位置2から位置5の手前までを取ります。TakeWhileは「条件を満たさなくなった時点」で終わります。Whereと違い、その後ろに条件を満たす要素(8)があっても含まれません。DistinctByはキーが同じ要素のうち最初の1件を残します。 分類ごとに先頭の商品が1件ずつ残りました。
変換する(Select・SelectMany・Chunk・Zip)
P("Select(Name)", products.Select(p => p.Name));
P("Select((p,i)=>...)", products.Take(3).Select((p, i) => $"{i}:{p.Name}"));
P("Select(匿名型)", products.Take(2).Select(p => new { p.Name, Tax = p.Price * 110 / 100 }));
P("SelectMany", new[] { "a b", "c d e" }.SelectMany(s => s.Split(' ')));
P("Chunk(3)", nums.Chunk(3).Select(c => "[" + string.Join(",", c) + "]"));
P("Zip", new[] { 1, 2, 3 }.Zip(new[] { "A", "B", "C" }, (n, s) => $"{n}{s}"));
P("Zip(タプル)", new[] { 1, 2 }.Zip(new[] { "A", "B" }));
P("Cast<int>()", new System.Collections.ArrayList { 1, 2, 3 }.Cast<int>());
実行結果(.NET 8):
Select(Name) => ノート, ボールペン, マウス, キーボード, 椅子, 机
Select((p,i)=>...) => 0:ノート, 1:ボールペン, 2:マウス
Select(匿名型) => { Name = ノート, Tax = 198 }, { Name = ボールペン, Tax = 132 }
SelectMany => a, b, c, d, e
Chunk(3) => [5,3,8], [3,1,8], [2]
Zip => 1A, 2B, 3C
Zip(タプル) => (1, A), (2, B)
Cast<int>() => 1, 2, 3
Selectは要素の数を変えずに、形だけを変えます。new { p.Name, Tax = ... }のように匿名型を使うと、必要な項目だけの入れ物をその場で作れます。SelectManyは、各要素から得られる複数の値を1本にまとめます。「注文ごとの明細を全部並べる」のような場面で使います。Chunk(3)は3件ずつの配列に分けます。 最後は余りの1件だけの配列になります。まとめて処理する件数を区切りたいときに便利です。Zipは短いほうの長さに合わせて終わります。
並べ替える(OrderBy・ThenBy・Order)
P("OrderBy(Price)", products.OrderBy(p => p.Price).Select(p => p.Name));
P("OrderByDescending(Price)", products.OrderByDescending(p => p.Price).Select(p => p.Name));
P("OrderBy.ThenByDescending", products.OrderBy(p => p.Category).ThenByDescending(p => p.Price).Select(p => p.Name));
P("Order()", nums.Order());
P("OrderDescending()", nums.OrderDescending());
P("Reverse()", nums.AsEnumerable().Reverse());
実行結果(.NET 8):
OrderBy(Price) => ボールペン, ノート, マウス, キーボード, 椅子, 机
OrderByDescending(Price) => 机, 椅子, キーボード, マウス, ノート, ボールペン
OrderBy.ThenByDescending => キーボード, マウス, 机, 椅子, ノート, ボールペン
Order() => 1, 2, 3, 3, 5, 8, 8
OrderDescending() => 8, 8, 5, 3, 3, 2, 1
Reverse() => 2, 8, 1, 3, 8, 3, 5
- 複数のキーで並べるときは、2つ目以降に
ThenByを使います。OrderByを2回続けると、後から書いた並べ替えが優先されてしまいます。 - 数値や文字列をそのままの値で並べるなら、
Order()がOrderBy(n => n)より短く書けます(.NET 7 以降)。 - LINQ の並べ替えは、キーが同じ要素どうしの元の順番を保ちます(安定な並べ替え)。
- 配列や
List<T>には同じ名前のReverseメソッドがあり、そちらは自分自身を書き換えます。新しい並びを結果として受け取りたいときは、上のようにAsEnumerable()を挟むと、LINQ のReverseであることが明確になります。
集計する(Count・Sum・Average・MaxBy・Aggregate)
V("Count()", products.Count());
V("Count(Stock==0)", products.Count(p => p.Stock == 0));
V("Sum(Price)", products.Sum(p => p.Price));
V("Average(Price)", products.Average(p => p.Price));
V("Min(Price)", products.Min(p => p.Price));
V("Max(Price)", products.Max(p => p.Price));
V("MinBy(Price)", products.MinBy(p => p.Price));
V("MaxBy(Price)", products.MaxBy(p => p.Price)?.Name);
V("Aggregate(合計)", nums.Aggregate((acc, n) => acc + n));
V("Aggregate(初期値あり)", nums.Aggregate("", (acc, n) => acc + n));
実行結果(.NET 8):
Count() => 6
Count(Stock==0) => 2
Sum(Price) => 46600
Average(Price) => 7766.666666666667
Min(Price) => 120
Max(Price) => 28000
MinBy(Price) => Product { Id = 2, Name = ボールペン, Category = 文具, Price = 120, Stock = 0 }
MaxBy(Price) => 机
Aggregate(合計) => 30
Aggregate(初期値あり) => 5383182
Max(p => p.Price)は「最大の価格(値)」、MaxBy(p => p.Price)は「価格が最大の商品(要素)」を返します。 「いちばん高い商品の名前」が欲しいときはMaxByが向いています。Count(条件)は、Where(条件).Count()と同じ結果を1回で書けます。Averageの結果は、intの集まりでもdoubleになります。Aggregateは、前回までの結果(acc)と次の要素(n)から新しい結果を作る処理を、先頭から順に繰り返します。初期値を渡すと、結果の型を要素の型と変えられます(ここでは文字列の連結)。
グループ化する(GroupBy・ToLookup)
foreach (var g in products.GroupBy(p => p.Category))
Console.WriteLine($" {g.Key}: {string.Join(", ", g.Select(p => p.Name))}");
P("GroupBy+集計", products.GroupBy(p => p.Category)
.Select(g => $"{g.Key}:件数{g.Count()}/合計{g.Sum(p => p.Price)}"));
P("GroupBy(key,結果)", products.GroupBy(p => p.Category, (key, items) => $"{key}:最高{items.Max(p => p.Price)}"));
var lookup = products.ToLookup(p => p.Category);
P("ToLookup[\"PC\"]", lookup["PC"].Select(p => p.Name));
V("ToLookup[\"なし\"].Count()", lookup["なし"].Count());
実行結果(.NET 8):
文具: ノート, ボールペン
PC: マウス, キーボード
家具: 椅子, 机
GroupBy+集計 => 文具:件数2/合計300, PC:件数2/合計6300, 家具:件数2/合計40000
GroupBy(key,結果) => 文具:最高180, PC:最高4800, 家具:最高28000
ToLookup["PC"] => マウス, キーボード
ToLookup["なし"].Count() => 0
GroupByの結果は「グループの集まり」です。 各グループはKey(分類)を持ち、グループ自体がそのキーに属する要素の集まりになっています。g.Count()やg.Sum(...)のように、グループに対してさらに LINQ を使えます。- グループは、キーが最初に現れた順に並びます。
ToLookupは、キーで何度も引きたいときに使います。存在しないキーを指定しても例外にならず、空の集まりが返る点がDictionaryとの違いです。
| やりたいこと | 書き方 |
|---|---|
| 分類ごとの件数 | GroupBy(p => p.Category).Select(g => new { g.Key, Count = g.Count() }) |
| 分類ごとの合計 | GroupBy(p => p.Category).Select(g => new { g.Key, Total = g.Sum(p => p.Price) }) |
| 分類ごとに最も高い商品 | GroupBy(p => p.Category).Select(g => g.MaxBy(p => p.Price)) |
| 複数の項目でグループ化 | GroupBy(p => new { p.Category, InStock = p.Stock > 0 }) |
| 件数が2件以上のグループだけ | GroupBy(...).Where(g => g.Count() >= 2) |
結合する(Join・GroupJoin・左外部結合)
P("Join(内部結合)", orders.Join(products, o => o.ProductId, p => p.Id, (o, p) => $"{o.Id}:{p.Name}x{o.Qty}"));
P("GroupJoin(商品ごとの注文数)", products.Take(3).GroupJoin(orders, p => p.Id, o => o.ProductId,
(p, os) => $"{p.Name}:{os.Count()}件"));
P("左外部結合", orders.GroupJoin(products, o => o.ProductId, p => p.Id, (o, ps) => new { o, ps })
.SelectMany(x => x.ps.DefaultIfEmpty(), (x, p) => $"{x.o.Id}:{p?.Name ?? "(該当なし)"}"));
P("Concat", new[] { 1, 2 }.Concat(new[] { 2, 3 }));
実行結果(.NET 8):
Join(内部結合) => 101:ノートx10, 102:マウスx2, 103:ノートx5, 104:椅子x1
GroupJoin(商品ごとの注文数) => ノート:2件, ボールペン:0件, マウス:1件
左外部結合 => 101:ノート, 102:マウス, 103:ノート, 104:椅子, 105:(該当なし)
Concat => 1, 2, 2, 3
Join の引数は「結合する相手、左側のキー、右側のキー、結果の作り方」の順です。
Joinは内部結合なので、相手が見つからない注文105は結果に出てきません。GroupJoinは、左側(商品)を1件ずつ残したまま、一致する右側(注文)の集まりを付けます。注文のないボールペンも「0件」として残ります。- 相手がいない行も残したい(左外部結合)ときは、
GroupJoinとSelectMany、DefaultIfEmptyを組み合わせます。相手がいない場合はpがnullになるので、p?.Name ?? "(該当なし)"のように扱います。
集合として扱う(Union・Intersect・Except)
int[] a = { 1, 2, 3, 4 }, b = { 3, 4, 5 };
P("Union", a.Union(b));
P("Intersect", a.Intersect(b));
P("Except", a.Except(b));
P("UnionBy(Category)", products.Take(2).UnionBy(products.Skip(2), p => p.Category).Select(p => p.Name));
P("ExceptBy(注文のある商品を除く)", products.ExceptBy(orders.Select(o => o.ProductId), p => p.Id).Select(p => p.Name));
P("IntersectBy(注文のある商品)", products.IntersectBy(orders.Select(o => o.ProductId), p => p.Id).Select(p => p.Name));
実行結果(.NET 8):
Union => 1, 2, 3, 4, 5
Intersect => 3, 4
Except => 1, 2
UnionBy(Category) => ノート, マウス, 椅子
ExceptBy(注文のある商品を除く) => ボールペン, キーボード, 机
IntersectBy(注文のある商品) => ノート, マウス, 椅子
Union は重複を取り除く点が、そのままつなげる Concat との違いです。
ExceptBy と IntersectBy は、2つ目の引数に「キーの集まり」を渡します。 「注文に出てくる商品Idに一致する商品」「一致しない商品」を、Where と Contains を組み合わせるより短く書けます。
要素を1つ取り出す(First・Single・ElementAt)
V("First()", nums.First());
V("First(n=>n>5)", nums.First(n => n > 5));
V("FirstOrDefault(n=>n>100)", nums.FirstOrDefault(n => n > 100));
V("FirstOrDefault(n>100, -1)", nums.FirstOrDefault(n => n > 100, -1));
V("Last()", nums.Last());
V("Single(n=>n==5)", nums.Single(n => n == 5));
V("SingleOrDefault(n=>n==9)", nums.SingleOrDefault(n => n == 9));
V("ElementAt(2)", nums.ElementAt(2));
V("ElementAt(^1)", nums.ElementAt(^1));
V("ElementAtOrDefault(99)", nums.ElementAtOrDefault(99));
P("DefaultIfEmpty(-1)", Array.Empty<int>().DefaultIfEmpty(-1));
実行結果(.NET 8):
First() => 5
First(n=>n>5) => 8
FirstOrDefault(n=>n>100) => 0
FirstOrDefault(n>100, -1) => -1
Last() => 2
Single(n=>n==5) => 5
SingleOrDefault(n=>n==9) => 0
ElementAt(2) => 8
ElementAt(^1) => 2
ElementAtOrDefault(99) => 0
DefaultIfEmpty(-1) => -1
FirstOrDefault は、見つからないときにその型の既定値(int なら 0、クラスなら null)を返します。0 が「見つからなかった」のか「値が0だった」のか区別しにくいときは、2つ目の引数で既定値を指定できます(.NET 6 以降)。ElementAt(^1) は末尾から数える書き方です。例外になる場合は、後の章でまとめて確認します。
判定する・生成する・コレクションにする
V("Any()", nums.Any());
V("Any(n=>n>7)", nums.Any(n => n > 7));
V("All(n=>n>0)", nums.All(n => n > 0));
V("Contains(8)", nums.Contains(8));
V("SequenceEqual", new[] { 1, 2 }.SequenceEqual(new[] { 1, 2 }));
V("空に対する All", Array.Empty<int>().All(n => n > 100));
V("空に対する Any", Array.Empty<int>().Any());
P("Range(1,5)", Enumerable.Range(1, 5));
P("Repeat(\"x\",3)", Enumerable.Repeat("x", 3));
V("Empty<int>().Count()", Enumerable.Empty<int>().Count());
P("Append(9)", nums.Take(2).Append(9));
P("Prepend(0)", nums.Take(2).Prepend(0));
V("ToList()", products.Where(p => p.Stock == 0).ToList().GetType().Name);
V("ToArray()", nums.ToArray().GetType().Name);
V("ToDictionary[3]", products.ToDictionary(p => p.Id)[3].Name);
V("ToDictionary(Id,Name)[5]", products.ToDictionary(p => p.Id, p => p.Name)[5]);
P("ToHashSet()", nums.ToHashSet());
実行結果(.NET 8):
Any() => True
Any(n=>n>7) => True
All(n=>n>0) => True
Contains(8) => True
SequenceEqual => True
空に対する All => True
空に対する Any => False
Range(1,5) => 1, 2, 3, 4, 5
Repeat("x",3) => x, x, x
Empty<int>().Count() => 0
Append(9) => 5, 3, 9
Prepend(0) => 0, 5, 3
ToList() => List`1
ToArray() => Int32[]
ToDictionary[3] => マウス
ToDictionary(Id,Name)[5] => 椅子
ToHashSet() => 5, 3, 8, 1, 2
- 「1件でもあるか」を調べるときは
Count() > 0よりAny()が向いています。Any()は1件見つかった時点で終わります。 - 空の集まりに対する
AllはTrueになります。 「条件に反する要素が1つもない」という意味だからです。「1件以上あって、すべて条件を満たす」を確かめたいときはAny()と組み合わせます。 Enumerable.Range(1, 5)の2つ目の引数は「終わりの値」ではなく「個数」です。ToDictionaryはキーが重複すると例外になります。重複がありうるときはToLookupやGroupByを使います。
クエリ構文とメソッド構文の対応
LINQ には、SQL に似たクエリ構文(from ... where ... select)と、ここまで使ってきたメソッド構文の2つの書き方があります。クエリ構文は、コンパイル時にメソッド構文に置き換えられるため、結果は同じです。
// 1. 絞り込み + 並べ替え + 変換
var q1 = from p in products
where p.Price >= 1000
orderby p.Price descending
select p.Name;
var m1 = products.Where(p => p.Price >= 1000).OrderByDescending(p => p.Price).Select(p => p.Name);
Console.WriteLine("クエリ : " + string.Join(", ", q1));
Console.WriteLine("メソッド: " + string.Join(", ", m1));
// 2. グループ化 + 集計
var q2 = from p in products
group p by p.Category into g
select new { Category = g.Key, Total = g.Sum(x => x.Price) };
var m2 = products.GroupBy(p => p.Category).Select(g => new { Category = g.Key, Total = g.Sum(x => x.Price) });
Console.WriteLine("クエリ : " + string.Join(", ", q2));
Console.WriteLine("メソッド: " + string.Join(", ", m2));
// 3. 結合 + 途中の計算(let)
var q3 = from o in orders
join p in products on o.ProductId equals p.Id
let amount = p.Price * o.Qty
where amount >= 1000
select $"{o.Id}:{p.Name}={amount}";
var m3 = orders.Join(products, o => o.ProductId, p => p.Id, (o, p) => new { o, p, amount = p.Price * o.Qty })
.Where(x => x.amount >= 1000)
.Select(x => $"{x.o.Id}:{x.p.Name}={x.amount}");
Console.WriteLine("クエリ : " + string.Join(", ", q3));
Console.WriteLine("メソッド: " + string.Join(", ", m3));
実行結果(.NET 8。3つ目は注文101〜103の3件で実行):
クエリ : 机, 椅子, キーボード, マウス
メソッド: 机, 椅子, キーボード, マウス
クエリ : { Category = 文具, Total = 300 }, { Category = PC, Total = 6300 }, { Category = 家具, Total = 40000 }
メソッド: { Category = 文具, Total = 300 }, { Category = PC, Total = 6300 }, { Category = 家具, Total = 40000 }
クエリ : 101:ノート=1800, 102:マウス=3000
メソッド: 101:ノート=1800, 102:マウス=3000
| クエリ構文 | メソッド構文 |
|---|---|
from p in products |
products(起点) |
where 条件 |
.Where(p => 条件) |
select 式 |
.Select(p => 式) |
orderby キー / orderby キー descending |
.OrderBy(...) / .OrderByDescending(...) |
orderby a, b |
.OrderBy(a).ThenBy(b) |
group p by キー into g |
.GroupBy(p => キー) |
join b in B on a.X equals b.Y |
.Join(B, a => a.X, b => b.Y, (a, b) => ...) |
join ... into g |
.GroupJoin(...) |
from a in A from b in a.Items |
.SelectMany(a => a.Items, ...) |
let x = 式 |
.Select(p => new { p, x = 式 }) |
短い絞り込みや変換、Count・First・Take・Distinct を使う処理はメソッド構文、結合が複数ある処理や let を使う処理はクエリ構文が読みやすくなります。クエリ構文の結果に (from ... select ...).ToList() のようにメソッドを続けることもできます。
遅延評価で結果が変わる例と ToList
LINQ の多くのメソッドは、呼んだ時点では処理をせず、結果が必要になったとき(foreach や ToList など)に初めて実行されます。 これを遅延評価と呼びます。この性質を知っておくと、「思っていた結果と違う」という場面の理由がわかります。
var list = new List<int> { 1, 2, 3 };
var lazy = list.Where(n => n >= 2); // ここではまだ実行されない
var fixedList = list.Where(n => n >= 2).ToList(); // ここで実行され、結果が確定する
list.Add(4);
Console.WriteLine("Where のみ : " + string.Join(", ", lazy));
Console.WriteLine("ToList 済み: " + string.Join(", ", fixedList));
int threshold = 2;
var byVar = list.Where(n => n >= threshold);
threshold = 4; // 列挙する前に変数を変更
Console.WriteLine("変数を後から変更: " + string.Join(", ", byVar));
実行結果(.NET 8):
Where のみ : 2, 3, 4
ToList 済み: 2, 3
変数を後から変更: 4
Where だけの lazy は、後から追加した 4 を含んでいます。lazy が持っているのは「結果」ではなく「list から2以上を取り出す、という手順」だからです。同じ理由で、ラムダ式の中で使っている変数(threshold)を後から変更すると、変更後の値で判定されます。その時点の結果を確定させたいときは、ToList() や ToArray() を呼びます。
実行される順番を確かめる
var seq = new[] { 1, 2, 3, 4 }
.Where(n => { Console.WriteLine($" Where {n}"); return n % 2 == 0; })
.Select(n => { Console.WriteLine($" Select {n}"); return n * 10; });
Console.WriteLine("クエリを組み立てました(まだ何も表示されない)");
foreach (var x in seq) Console.WriteLine($" 結果 {x}");
実行結果(.NET 8):
クエリを組み立てました(まだ何も表示されない)
Where 1
Where 2
Select 2
結果 20
Where 3
Where 4
Select 4
結果 40
「全件を Where してから全件を Select」ではなく、1件ずつ Where → Select → 結果、と流れていきます。 途中に Take(1) があれば、1件見つかった時点で残りは処理されません。
| 種類 | メソッドの例 | 実行されるタイミング |
|---|---|---|
| 遅延評価 | Where・Select・SelectMany・Take・Skip・Distinct・Concat・Chunk・Zip |
列挙したとき。1件ずつ流れる |
| 遅延評価(全件を読んでから返す) | OrderBy・GroupBy・Reverse・Join・Except |
列挙したとき。最初の1件を返す前に全体を読む |
| すぐに実行 | ToList・ToArray・ToDictionary・ToLookup・ToHashSet |
呼んだとき。コレクションを返す |
| すぐに実行 | Count・Sum・Max・First・Any・All・Aggregate |
呼んだとき。1つの値を返す |
戻り値が IEnumerable<T> なら遅延評価、1つの値やコレクションならすぐに実行、と覚えると見分けやすくなります。
複数回列挙の落とし穴
遅延評価のクエリは、列挙するたびに最初から実行し直されます。 同じクエリに Count()・Sum()・Max() を続けて呼ぶと、そのたびに全体が計算されます。
int calls = 0;
IEnumerable<int> expensive = new[] { 1, 2, 3 }.Select(n => { calls++; return n * n; });
Console.WriteLine($"Count={expensive.Count()}, Sum={expensive.Sum()}, Max={expensive.Max()}");
Console.WriteLine($"ラムダが呼ばれた回数(ToListなし): {calls}");
calls = 0;
var cached = new[] { 1, 2, 3 }.Select(n => { calls++; return n * n; }).ToList();
Console.WriteLine($"Count={cached.Count}, Sum={cached.Sum()}, Max={cached.Max()}");
Console.WriteLine($"ラムダが呼ばれた回数(ToListあり): {calls}");
var rnd = new Random(1);
var randoms = Enumerable.Range(0, 3).Select(_ => rnd.Next(100));
Console.WriteLine("1回目: " + string.Join(", ", randoms));
Console.WriteLine("2回目: " + string.Join(", ", randoms));
実行結果(.NET 8):
Count=3, Sum=14, Max=9
ラムダが呼ばれた回数(ToListなし): 9
Count=3, Sum=14, Max=9
ラムダが呼ばれた回数(ToListあり): 3
1回目: 24, 11, 46
2回目: 77, 65, 43
3件のデータに対して、ToList なしではラムダ式が9回(3件×3回の列挙)呼ばれました。ToList で一度確定させれば3回で済みます。変換の中身がデータベースへの問い合わせや時間のかかる計算だった場合、この差はそのまま処理時間の差になります。
乱数の例では、同じ変数を2回表示しただけで中身が変わっています。 列挙のたびに rnd.Next が呼び直されるためです。現在時刻や連番の採番のように「呼ぶたびに結果が変わる処理」を含むクエリは、ToList() で確定させてから使うと、何度参照しても同じ値になります。
列挙の途中で元のコレクションを変更したとき
var src = new List<int> { 1, 2, 3 };
foreach (var n in src.Where(n => n > 1)) src.Remove(n);
実行結果(.NET 8):
InvalidOperationException: Collection was modified; enumeration operation may not execute.
Where は src を読みながら進むので、途中で src を変更すると例外になります。src.Where(n => n > 1).ToList() としてから foreach するか、src.RemoveAll(n => n > 1) を使うと意図どおりに動きます。
First・Single の例外と OrDefault の使い分け
要素を取り出すメソッドや集計メソッドは、対象がないときの動きがメソッドによって違います。実際のメッセージを一覧にします(.NET 8 で実行。empty は空の int[]、nums は { 5, 3, 8, 3 })。
| 呼び出し | 結果 |
|---|---|
empty.First() |
InvalidOperationException: Sequence contains no elements |
nums.First(n => n > 100) |
InvalidOperationException: Sequence contains no matching element |
nums.Single() |
InvalidOperationException: Sequence contains more than one element |
nums.Single(n => n == 3) |
InvalidOperationException: Sequence contains more than one matching element |
nums.SingleOrDefault(n => n == 3) |
InvalidOperationException: Sequence contains more than one matching element |
empty.FirstOrDefault() |
0 |
empty.FirstOrDefault(-1) |
-1 |
products.FirstOrDefault(p => p.Id == 99) |
null |
empty.Max() / empty.Average() |
InvalidOperationException: Sequence contains no elements |
empty.Sum() |
0 |
empty.DefaultIfEmpty(0).Max() |
0 |
empty.Cast<int?>().Max() |
null |
products.Where(p => p.Price < 0).MaxBy(p => p.Price) |
null |
empty.Aggregate((a, b) => a + b) |
InvalidOperationException: Sequence contains no elements |
nums.ElementAt(10) |
ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index') |
products.ToDictionary(p => p.Category) |
ArgumentException: An item with the same key has already been added. Key: 文具 |
((int[])null).Where(n => n > 0) |
ArgumentNullException: Value cannot be null. (Parameter 'source') |
new object[] { 1, "a" }.Cast<int>().ToList() |
InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Int32'. |
読み取れるポイントは次のとおりです。
SingleOrDefaultは「0件なら既定値」ですが、「2件以上」のときは例外になります。OrDefaultが付いていても、複数件は許されません。- 空の集まりに対して、
Sumは0を返し、Max・Min・Averageは例外になります(要素の型がintなどの値型の場合)。空になりうるときは、DefaultIfEmpty(0).Max()のように既定値を用意します。 - 要素がクラスや
int?のようにnullを取れる型なら、Max・MaxByは空のときnullを返します。
| メソッド | 0件 | 1件 | 2件以上 | 使う場面 |
|---|---|---|---|---|
First |
例外 | その要素 | 最初の要素 | 必ず1件以上あるはずのとき |
FirstOrDefault |
既定値 | その要素 | 最初の要素 | ない場合もあるとき(主キー以外での検索など) |
Single |
例外 | その要素 | 例外 | ちょうど1件のはずで、違えば気づきたいとき |
SingleOrDefault |
既定値 | その要素 | 例外 | 0件か1件のはずのとき(Idでの検索など) |
FirstOrDefault の結果がクラスの場合は null になりうるので、if (item is null) や item?.Name で確認してから使います。
.NET 6〜9 で追加されたメソッドの対応バージョン表
LINQ のメソッドは .NET のバージョンとともに増えています。.NET 6 と 7 の行は一般に知られた追加時期で、メソッドの動作は今回 .NET 8 で実行して確かめました。.NET 9 の3つは、Microsoft Learn の「.NET 9 の新機能(ライブラリ)」のページで確認した内容です(.NET 8 では実行できないため未実行)。
| メソッド | 使える版 | 働き | 今回の確認 |
|---|---|---|---|
Chunk(n) |
.NET 6 以降 | n件ずつの配列に分ける | .NET 8 で実行 |
DistinctBy / UnionBy / IntersectBy / ExceptBy |
.NET 6 以降 | キーで判定する重複除去・集合演算 | .NET 8 で実行 |
MinBy / MaxBy |
.NET 6 以降 | キーが最小/最大の要素を返す | .NET 8 で実行 |
Take(2..5)(範囲指定) |
.NET 6 以降 | 範囲で取り出す | .NET 8 で実行 |
ElementAt(^1)(末尾から) |
.NET 6 以降 | 末尾から数えて取り出す | .NET 8 で実行 |
FirstOrDefault(既定値) など |
.NET 6 以降 | 見つからないときの値を指定する | .NET 8 で実行 |
TryGetNonEnumeratedCount |
.NET 6 以降 | 列挙せずに件数がわかる場合だけ取得する | .NET 8 で実行 |
Order() / OrderDescending() |
.NET 7 以降 | 要素そのものの値で並べる | .NET 8 で実行 |
CountBy |
.NET 9 以降 | キーごとの件数を返す | 公式ドキュメントで確認 |
AggregateBy |
.NET 9 以降 | キーごとに Aggregate する |
公式ドキュメントで確認 |
Index() |
.NET 9 以降 | (位置, 要素) の組にする | 公式ドキュメントで確認 |
.NET 8 のプロジェクトで .NET 9 のメソッドを書くと、次のコンパイルエラーになります(.NET 8 で実行)。
Program.cs(3,25): error CS1061: 'int[]' does not contain a definition for 'CountBy' and no accessible extension method 'CountBy' accepting a first argument of type 'int[]' could be found (are you missing a using directive or an assembly reference?)
.NET 9 のメソッドと同じ結果を .NET 8 で書く
.NET 8 では、GroupBy と Select の組み合わせで同じ結果を得られます。
// .NET 9: products.CountBy(p => p.Category)
var countBy = products.GroupBy(p => p.Category).Select(g => KeyValuePair.Create(g.Key, g.Count()));
Console.WriteLine("CountBy 相当 : " + string.Join(", ", countBy));
// .NET 9: products.AggregateBy(p => p.Category, 0, (sum, p) => sum + p.Price)
var aggBy = products.GroupBy(p => p.Category)
.Select(g => KeyValuePair.Create(g.Key, g.Aggregate(0, (sum, p) => sum + p.Price)));
Console.WriteLine("AggregateBy 相当: " + string.Join(", ", aggBy));
// .NET 9: products.Take(3).Index()
var index = products.Take(3).Select((p, i) => (Index: i, Item: p.Name));
Console.WriteLine("Index 相当 : " + string.Join(", ", index));
実行結果(.NET 8):
CountBy 相当 : [文具, 2], [PC, 2], [家具, 2]
AggregateBy 相当: [文具, 300], [PC, 6300], [家具, 40000]
Index 相当 : (0, ノート), (1, ボールペン), (2, マウス)
公式ドキュメントによると、CountBy と AggregateBy はキーと値の組(KeyValuePair)の集まりを返し、Index() は foreach ((int index, string line) in lines.Index()) のように位置と要素を同時に受け取れます。.NET 9 以降では、グループを途中で作らずに済むぶん、短く書けるようになります。
よくある質問
Q. LINQ と for 文はどちらを使えばよいですか?
A. 絞り込み・変換・集計のように「何を取り出すか」を表す処理は LINQ が読みやすくなります。要素の位置を細かく操作する処理や、途中で複数の変数を更新していく処理は for や foreach のほうが素直に書けます。両方を混ぜても問題ありません。
Q. ToList と ToArray はどちらを使えばよいですか?
A. 後から要素を追加・削除するなら ToList()、件数が変わらない結果として持つなら ToArray() が目安です。どちらも呼んだ時点でクエリを実行し、結果を確定させる点は同じです。
Q. クエリ構文とメソッド構文はどちらを覚えればよいですか?
A. まずメソッド構文を覚えるのがおすすめです。Count・First・Take・Distinct など、メソッド構文でしか書けない操作が多いためです。結合が複数ある処理ではクエリ構文が読みやすい場合があるので、読めるようにしておくと役立ちます。
まとめ
- LINQ は、絞り込み(
Where)・変換(Select)・並べ替え(OrderBy)・集計(Sumなど)・グループ化(GroupBy)を同じ書き方でつなげられる機能 GroupByの結果はグループの集まり。g.Keyでキー、g.Count()やg.Sum(...)でグループごとの集計ができるWhereやSelectは遅延評価。 列挙した時点で実行されるので、結果を確定させたいとき・複数回使うときはToList()を呼ぶFirstとSingleは対象がないとInvalidOperationException。 ない場合があるならFirstOrDefault、0件か1件のはずならSingleOrDefault- 空の集まりに対して
Sumは 0、Max・Averageは例外。DefaultIfEmpty(0)で既定値を用意できる - 複数キーの並べ替えは
OrderBy(...).ThenBy(...)、キーでの重複除去はDistinctBy、最大の要素はMaxBy Chunk・DistinctBy・MaxByは .NET 6 以降、Orderは .NET 7 以降、CountBy・AggregateBy・Indexは .NET 9 以降で使える
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C# LINQの使い方一覧|Where・Select・GroupByを実行結果つきで」
株式会社RJCでは、C#のLINQのような日々使う書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。