Menu

C#のLINQ:Where、Select、OrderBy、GroupByなどの使い方

LINQはC#のすべてのコレクションにクエリ演算子を加えます。メソッド構文とクエリ構文、Whereによる絞り込み、Selectによる射影、OrderByとThenByによる並べ替え、GroupByによるグループ化、FirstとFirstOrDefaultの落とし穴、集計、そして遅延実行によってクエリが実行されるタイミングがどう変わるかを学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

LINQ(Language Integrated Query)は、シーケンスの絞り込み、変換、並べ替え、グループ化、集計のためのメソッドのライブラリです。using System.Linq; を追加すると、すべての配列、List<T>、Dictionary、その他の IEnumerable<T> に Where、Select、OrderBy などのメソッドが加わり、それぞれ1つの要素をどう扱うかを表すラムダを受け取ります。

最初のクエリ:メソッド構文とクエリ構文

LINQには2つの記法があります。メソッド構文は呼び出しを連結し、クエリ構文はSQLのように読めます。コンパイラはクエリ構文を同じメソッド呼び出しに変換するので、結果は同じです。

出力:

95, 91, 88
95, 91, 88

現代的なコードではメソッド構文のほうが一般的で、一部の演算子(Take、Distinct、Count、First、ToList)はメソッドとしてしか存在しません。クエリ構文が真価を発揮するのは、結合や、let で中間変数を導入するクエリです。このページの残りではメソッド構文を使い、クエリ構文が役に立つところではそれも示します。

WhereとSelect:絞り込みと射影

Where は条件に一致する要素を残します。Select は各要素を別のものに変換します。この2つで日常的なクエリのほとんどをまかなえます。

出力:

Mug, Kettle
MUG $8.50 | LAMP $32.00 | KETTLE $27.00 | RUG $120.00
Lamp: 38.40
Rug: 144.00
1. Mug 2. Lamp 3. Kettle 4. Rug

Where は要素を決して変更せず、Select は要素の数を決して変えません。new { p.Name, WithTax = ... } は匿名型を作ります。コンパイラが代わりに書いてくれるクラスで、そのプロパティは通常のドット構文で読みます。書ける名前がないので、それを保持する変数は var で宣言する必要があり、1つのメソッドの中にとどまる結果に向いています。

OrderBy、ThenBy、複数のキーによる並べ替え

出力:

Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana

OrderBy は安定です。キーが等しい要素は元の順序のままです。2つ目のキーには ThenBy か ThenByDescending を使います。2回目の OrderBy は、すべてを自分のキーで並べ替え直すので、最初のキーは同順のときの決め手にしかならず、優先度が逆になります。クエリ構文では、キーをカンマで区切ります:orderby o.Customer, o.Total descending。

List<T>.Sort と違い、OrderBy はソースに触れず、新しいシーケンスを返します。

First、Single、LastとそのOrDefault版

これらはシーケンスではなく1つの要素を返します。通常版は返すものがないと例外を投げ、OrDefault 版はその型の既定値を返します。

出力:

Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0

空のシーケンスに対する First の例外は、メッセージが「Sequence contains no elements」(述語に何も一致しなかった場合は「Sequence contains no matching element」)の InvalidOperationException で、C#で最も検索されるエラーの1つです。「何もない」が通常の結果なら FirstOrDefault を使い、null を確認します。

最後の行は値型の落とし穴を示しています。int のシーケンスでは、何も一致しないと FirstOrDefault は 0 を返し、本物の 0 と区別できません。Where で絞り込んで先に Any() を確認するか、.NET 6以降なら独自の既定値を渡します:scores.FirstOrDefault(s => s > 100, -1)。

Single は「ちょうど1つ」のためのものです。1つもない場合も複数ある場合も例外を投げるので、クエリであると同時にアサーションでもあります。

Any、All、Count、Contains

出力:

True
True
True
3
True
True

Any() は最初に一致したところで止まるので、「何かあるか」を尋ねるには items.Any() が正しい方法です。items.Count() > 0 は、ソースがリストや配列のようなコレクションなら Count プロパティを読みますが、クエリなどの遅延評価されるシーケンスでは、まずすべての要素をたどります。空のシーケンスに対する All は true を返します。条件を満たさない要素が1つもないからです。

Sum、Average、Min、Max

出力:

Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException

空のシーケンスの Sum は 0 ですが、空の数値シーケンスの Average、Min、Max は、意味のある答えがないので InvalidOperationException を投げます。Any() で保護するか、先に DefaultIfEmpty() を使います:empty.DefaultIfEmpty(0).Average()。

Distinct、Take、Skip

出力:

home, shop, cart
Page 1: 1 2 3 4 5 6 7 8 9 10
Page 2: 11 12 13 14 15 16 17 18 19 20
Page 3: 21 22 23
12, 15

Distinct は各値の最初の出現を順序どおりに残します。Skip(n).Take(m) は標準的なページングの形で、TakeWhile/SkipWhile は条件を満たさない最初の要素で止まったり始まったりします。Enumerable.Range(start, count) は、配列なしで整数のシーケンスを生成します。

GroupBy

GroupBy は要素をキーによってバケットに振り分けます。各グループは Key を持ち、それ自体がシーケンスなので、集計できます。

出力:

North: 3 sales, total 600
South: 2 sales, total 400
East: 1 sales, total 350
Top rep: Ana 400
North -> Ana/Chloe/Ana; South -> Ben/Eli; East -> Dev

グループは並べ替えられずに、キーがソースに最初に現れた順(North、South、East)で出てきます。並べ替えが必要なら OrderBy(g => g.Key) を追加します。クエリ構文では、group X by K で各グループに入れるもの(ここでは担当者の名前だけ)を選び、into g でグループに名前を付けてクエリを続けられるようにします。

ToList、ToArray、ToDictionary

To... メソッドはクエリを実行して結果を保存します。

出力:

ben@x.com
2
ToDictionary: ArgumentException

ToDictionary は、Dictionary.Add と同じく、2つの要素が同じキーを生成すると ArgumentException を投げます。重複が予想されるなら、(wordCounts のように)先にグループ化するか、各キーを値のシーケンスに対応付ける ToLookup を使います。

遅延実行

Where、Select、OrderBy、GroupBy をはじめほとんどの演算子はクエリオブジェクトを返し、何かがそれを列挙するまで何の処理もしません。これには確認しておくべき2つの結果があります。

出力:

25, 40, 99
  converting 10
  converting 25
  converting 40
  converting 99
Count: 4
  converting 10
  converting 25
  converting 40
  converting 99
Max: 9900
  converting 10
  converting 25
  converting 40
  converting 99
Cached: 4, 9900

最初のクエリは、クエリを書いた後に追加された 99 を反映しました。絞り込みは string.Join が列挙したときに実行されたからです。2つ目のクエリは変換のメッセージを2回表示しました。Count() と Max() がそれぞれ最初から列挙したからです。軽いラムダなら処理が無駄になるだけですが、ラムダの中にデータベースのクエリ、ファイルの読み込み、乱数があると、2回の走査で異なるデータが返ることがあります。結果を複数回使うなら、ToList() か ToArray() で実体化します。

1つの値を返す演算子(Count、Sum、First、Any、Max)と To... メソッドはすぐに実行されます。この遅延評価がどう作られているかはIEnumerableとyieldを参照してください。

早見表

演算子処理戻り値
Where(x => cond)絞り込みシーケンス(遅延)
Select(x => expr)各要素を変換シーケンス(遅延)
SelectMany(x => seq)入れ子のシーケンスを平坦化シーケンス(遅延)
OrderBy、ThenBy(+Descending)並べ替え、安定シーケンス(遅延)
GroupBy(x => key)キーで振り分けグループのシーケンス(遅延)
Distinct、Take、Skip重複除去、ページングシーケンス(遅延)
First、Single、Last1つの要素、なければ例外要素
FirstOrDefault など1つの要素または既定値要素
Any、All、Contains判定bool
Count、Sum、Min、Max、Average集計数値
ToList、ToArray、ToDictionary実行して保存コレクション

よくある間違い

  • 空になりうるシーケンスに First()。 「Sequence contains no elements」が投げられます。FirstOrDefault を使って確認します。
  • OrderBy を2回呼ぶ。 2つ目のキーが主キーになります。ThenBy を使います。
  • クエリを何度も列挙する。 処理が繰り返されます。ToList() を1回呼びます。
  • クエリがソースのスナップショットを取ると思う。 クエリは書いたときではなく、列挙されたときにソースを読みます。
  • 空のシーケンスに Average、Min、Max。 例外が投げられます。Any() か DefaultIfEmpty で保護します。
  • 要素があるかを Count() > 0 で調べる。 Any() なら最初の1つで止まります。

よくある質問

C#のLINQとは何ですか?

LINQ(Language Integrated Query)は、Where、Select、OrderBy、GroupBy など、System.Linq にある拡張メソッドの集まりで、配列、リスト、辞書、他のクエリの結果など、任意の IEnumerable<T> に使えます。C#にはクエリ構文(from x in items where ... select ...)もあり、コンパイラはそれを同じメソッド呼び出しに変換します。

LINQのSelectとWhereの違いは何ですか?

Where は絞り込みで、ラムダが true を返す要素を変更せずに残します。Select は射影で、すべての要素をプロパティや新しいオブジェクトなど別のものに変換し、要素数は変えません。よく連結して使います:people.Where(p => p.Age >= 18).Select(p => p.Name)。

FirstとFirstOrDefaultの違いは何ですか?

First() は最初の要素(述語があれば最初に一致したもの)を返し、何もない場合は InvalidOperationException(「Sequence contains no elements」または「no matching element」)を投げます。FirstOrDefault() は代わりにその型の既定値を返します。クラスなら null、int なら 0 なので、使う前に結果を確認します。

LINQで2つのフィールドで並べ替えるには?

OrderBy の後に ThenBy を連結します:orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total)。OrderBy を2回呼ぶと期待どおりになりません。2回目の呼び出しがシーケンス全体を並べ替え直すので、そのキーが主キーになり、最初のキーは同順のときの決め手にしかならず、意図とは逆になります。

LINQのGroupByはどう動きますか?

GroupBy(x => key) は、異なるキーごとに1つのグループを返します。各グループは IGrouping<TKey, TElement> で、Key プロパティを持ち、それ自体がそのキーを持つ要素のシーケンスなので、Count()、Sum()、Select を呼び出せます。グループは、キーが最初に現れた順に出てきます。

LINQの遅延実行とは何ですか?

ほとんどのLINQ演算子は呼び出した時点では実行されず、列挙されるたび(foreach、ToList()、Count() など)に実行されるクエリを返します。そのため、クエリは定義した後にソースに加えられた変更を反映し、2回列挙すると処理も2回行われます。1回だけ実行して結果を保持するには、ToList() か ToArray() を呼びます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める