ラムダ式は、自分が定義されたスコープの変数を「取り込んで」使えます。この動作を変数キャプチャ(またはクロージャ)と呼びます。便利な反面、ループ内で使うときにはまりやすい落とし穴があります。
このページを読み終えると、以下のことができるようになります。
for ループ内のキャプチャの罠とその回避方法を書けるstatic ラムダで意図しないキャプチャを禁止できる通常のメソッドは、自分のパラメータとローカル変数しか使えません。ラムダ式は、それに加えて定義された時点で見えていた外側の変数もそのまま使えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
public class Program
{
public static void Main()
{
string greeting = "こんにちは";
Action<string> greet = name => Console.WriteLine($"{greeting}、{name}!");
greet("Alice");
greeting = "おはよう"; // 外側の変数を変更
greet("Bob"); // ラムダ式は変更後の値を参照する
}
}
1
2
こんにちは、Alice!
おはよう、Bob!
greeting はラムダ式の外で定義されていますが、ラムダ式の中から参照できています。また、greeting を書き換えた後に greet("Bob") を呼ぶと、更新後の値 "おはよう" が使われます。これはラムダ式がコピーではなく変数そのものを保持しているためです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
public class Program
{
public static void Main()
{
int count = 0;
Action increment = () => count++;
increment();
increment();
increment();
Console.WriteLine(count); // 外側の count が変更されている
}
}
1
3
count++ はラムダ式の外の count を直接インクリメントしています。ラムダ式が count をコピーしているのではなく、元の変数を共有していることがわかります。
for ループ内のキャプチャの罠ループ変数をキャプチャするときに、意図しない動作になりやすい罠があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action();
}
}
}
1
2
3
3
3
3
ループが完了すると i の値は 3 になっています。3 つのラムダ式がすべて同じ変数 i を参照しているため、呼び出し時点の値(3)が表示されます。
ループごとに値を固定するには、ループ内に新しい変数を作ってキャプチャします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int captured = i; // ループ毎に新しい変数を作る
actions.Add(() => Console.WriteLine(captured));
}
foreach (var action in actions)
{
action();
}
}
}
1
2
3
0
1
2
captured はループの各イテレーションで新しく生成されるため、それぞれ異なる変数をキャプチャします。
💡 ポイント:
foreachのループ変数は C# 5 以降、各イテレーションで新しい変数として扱われるためこの問題は起きません。forループのint iに注意してください。
static ラムダC# 9 から、ラムダ式に static を付けると外側の変数や this をキャプチャしようとしたときにコンパイルエラーにできます。意図せずキャプチャが発生するのを防ぎたい場面で使います。
書式:static ラムダ
1
static (パラメータ) => 式
| 要素 | 説明 |
|---|---|
static |
キャプチャを禁止するキーワード |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
public class Program
{
public static void Main()
{
int multiplier = 3;
// ❌ NG: static ラムダで外側の変数をキャプチャしようとするとコンパイルエラー
// Func<int, int> badLambda = static x => x * multiplier;
// ✅ OK: パラメータだけを使う
Func<int, int, int> multiply = static (x, factor) => x * factor;
Console.WriteLine(multiply(5, multiplier));
}
}
1
15
1
2
3
4
5
6
7
8
9
10
11
12
// ❌ NG: for ループ変数を直接キャプチャすると、全ラムダが同じ変数を参照する
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i)); // 全部 3 になる
}
// ✅ OK: ループ内で新しい変数にコピーしてキャプチャする
for (int i = 0; i < 3; i++)
{
int captured = i;
actions.Add(() => Console.WriteLine(captured)); // 0, 1, 2 になる
}
for ループのループ変数を直接キャプチャすると全ラムダが最終値を参照する罠があるstatic ラムダ(C# 9)でキャプチャを禁止してコンパイル時に意図しない参照を検出できる以下の問いに答えられるか確認しましょう。
次のコードの出力結果は何になりますか?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var actions = new List<Action>();
for (int i = 0; i < 4; i++)
{
int n = i * 2;
actions.Add(() => Console.WriteLine(n));
}
foreach (var a in actions) a();
}
}
int n = i * 2; の行を削除し、代わりに () => Console.WriteLine(i * 2) と書いた場合、出力結果はどう変わりますか?理由も答えてください。参照です。キャプチャした変数が後から書き換えられると、ラムダ式が次に呼ばれたときに新しい値が使われます。
1
2
3
4
0
2
4
6
各イテレーションで n という新しい変数が作られるため、それぞれ 0, 2, 4, 6 がキャプチャされます。
8 が出力されます(i の最終値 4 に対し 4 * 2 = 8)。ループ終了後に i は 4 になっており、3 つのラムダが同じ i を参照しているためです。ローカル関数 では、メソッドの内側に定義できるメソッドと、ラムダ式との使い分けを学びます。