Menu

C#のラムダ式:構文、FuncとAction、クロージャ、LINQ

C#のラムダ式の仕組みを解説します。=>構文のあらゆる形、FuncとActionへのラムダの格納、メソッドやLINQへの受け渡し、ステートメント形式のラムダ、キャプチャした変数に対するクロージャ、そしてforeachにはないforループのキャプチャのバグを扱います。

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

ラムダ式は、=> 演算子(「goes to」)を使ってその場で書く小さな匿名関数です。左に引数、右に結果を書きます。

出力:

60.0
12
ORDER SHIPPED!
96.0, 180.0

ラムダ自体には型がありません。文脈が期待するデリゲート型のデリゲート(関数を指すオブジェクト)になります。ここでは Func<decimal, decimal>、List.FindAll なら Predicate<T>、イベントなら EventHandler です。Func デリゲートは値を返し、Action デリゲートは返しません。一覧はFuncとActionのページにあります。

構文の形

x => x * x                         // one parameter: parentheses optional
(x, y) => x + y                    // several parameters
() => DateTime.Now.Year            // no parameters
(int x, string s) => s.Length > x  // explicit parameter types
order =>                           // statement lambda: a block with return
{
    decimal subtotal = order.Qty * order.Price;
    return subtotal > 100m ? subtotal * 0.9m : subtotal;
}

式形式のラムダは => の後に1つの式を持ち、その値が結果になります。ステートメント形式のラムダは波かっこで囲んだブロックを持ち、任意の文を含められます。デリゲートが値を返すなら、すべての経路で値を return しなければなりません(そうでなければCS1643 Not all code paths return a value in lambda expression)。

新しいバージョンでは、さらに形が追加されました。どれも任意です。

  • C# 9:破棄の引数。使わない複数の引数をすべて _ にできます((_, _) => Save())。また、変数のキャプチャを禁止された static ラムダ。
  • C# 10:「自然な型」。var square = (int x) => x * x; から Func<int, int> が推論されます。それ以前は var f = x => x * 2; は失敗し、C# 10でも引数の型がなければCS8917 The delegate type could not be inferred で失敗します。
  • C# 12:引数の既定値。(int x, int y = 1) => x + y。

メソッドにラムダを渡す

デリゲートの引数を取るメソッドなら、どれでもラムダを受け付けます。こうしてデータだけでなく振る舞いを渡します。

出力:

report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf

Filter は「残す」が何を意味するかを知りません。それを決めるのは、ラムダを渡す呼び出し側です。List.Sort も同じ仕組みで、ラムダは負、ゼロ、正を返す比較で、Sort は必要なだけそれを呼び出します。

LINQのラムダ

C#のコードがラムダに出会う場所の多くはLINQです。どのクエリ演算子もラムダを受け取ります。Where は条件、Select は射影、OrderBy はキー、Sum は合計する値です。

出力:

Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45

ラムダは書かれた場所では実行されません。LINQはそれを保存しておき、クエリが foreach、ToList()、Sum() で列挙されたときに呼び出します。これを遅延実行と呼びます。演算子そのものはLINQのページで扱います。

クロージャ:キャプチャされた変数

ラムダは、それが書かれたメソッドのローカル変数や引数を使えます。ラムダがキャプチャするのは現在の値ではなく変数で、変数はラムダが存在する限り生き続けます。

出力:

1
2
3
1
50.0

MakeCounter を呼ぶたびに新しい count が作られるので、2つのカウンターは独立しています。割引のラムダは、書かれたときではなく実行されたときに rate を読んだので、50%を適用しました。コンパイラは、キャプチャされた変数をメソッドとラムダが共有する隠しクラスに移すことでこれを実現しています。

ループでのキャプチャの落とし穴

ループ変数のキャプチャは、クロージャで痛い目にあう場面です。for ループにはループ全体で1つの変数しかないので、その中で作られたすべてのラムダがその変数を共有します。

出力:

3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach

最初のループの3つのラムダはすべて、ループが終わって i が3になった後に実行されました。本体の中で宣言した変数に i をコピーすると、各ラムダが自分の変数を持ちます。foreach は自動的に正しく動きます。C# 5以降、反復変数は毎回新しい変数だからです。C# 5より前は同じバグがあったので、ネット上の古い回答では今でも foreach でもコピーを勧めています。

同じ落とし穴は、後で実行されるものすべてに当てはまります。イベントハンドラー、タイマー、Task.Run、ループの中で組み立ててループの後で列挙するLINQクエリなどです。

ラムダ、匿名メソッド、ローカル関数

ラムダはC# 2の匿名メソッドの構文に取って代わりましたが、匿名メソッドも今でもコンパイルできます。

Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; };   // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0;                          // lambda

1つのメソッドの中だけで使うヘルパーなら、C# 7.0のローカル関数もあります。名前を持ち、再帰でき、デリゲートに変換しない限りデリゲートを確保しません。何か(LINQ、Sort、イベント)に振る舞いを渡すならラムダを、自分で呼び出すならローカル関数かprivateなメソッドを使います。

Entity FrameworkのようなLINQプロバイダーでは、もう1つ区別が現れます。Expression<Func<T, bool>> に代入されたラムダは、コードにはコンパイルされず、コードを表すデータ構造にコンパイルされ、プロバイダーがそれをSQLに変換します。構文は同じで、どちらになるかは、呼び出すメソッドの引数の型で決まります。

よくある質問

C#のラムダ式とは何ですか?

=> 演算子で書く匿名関数です。x => x * 2 は x を受け取り x * 2 を返します。ラムダ自体には名前がなく、Func<int, int> や Action<string> のようなデリゲート型に変換され、変数に格納したり、メソッドに渡したり、メソッドから返したりできます。

引数が複数またはゼロのラムダはどう書きますか?

引数をかっこで囲みます。2つなら (a, b) => a + b、ゼロなら () => DateTime.Now です。かっこを省けるのは引数が1つの場合だけです(x => x + 1)。型を明示することもできます:(int a, int b) => a + b。

C#のラムダにおけるクロージャとは何ですか?

周りのメソッドのローカル変数を使うラムダは、その値のスナップショットではなく、変数そのものをキャプチャします。変数はラムダが存在する限り生き続け、後でどちらかが変更すると、もう一方にもその変更が見えます。これがカウンターやコールバックを動かす仕組みであり、典型的な for ループのキャプチャのバグの原因でもあります。

forループの中のラムダがすべて同じ値になるのはなぜですか?

for ループにはループ全体で1つのループ変数しかないので、中で作られたすべてのラムダが同じ i をキャプチャし、後で実行されたときにその最終値を見るからです。本体の中でローカル変数にコピーし(int copy = i;)、そのコピーをキャプチャします。C# 5以降の foreach にはこの問題はありません。反復ごとに新しい変数を作るからです。

var f = x => x * 2 がコンパイルできないのはなぜですか?

コンパイラには、どのデリゲート型が望まれているのか、x が何の型なのかがわからないので、CS8917 The delegate type could not be inferred(C# 10より前はCS0815)を報告します。型を宣言する(Func<int, int> f = x => x * 2;)か、C# 10以降なら引数に型を付けます:var f = (int x) => x * 2;。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める