C# ラムダ式のキャプチャとクロージャ

ラムダ式は、自身の外側で定義された変数を参照できます。この機能を「キャプチャ」と呼び、キャプチャされた変数を含むラムダ式を「クロージャ」と呼びます。

変数のキャプチャ

ラムダ式は、その定義時点でスコープ内にある変数を取り込むことができます。

int multiplier = 3;

Func<int, int> multiply = x => x * multiplier;

Console.WriteLine(multiply(5));  // 15
Console.WriteLine(multiply(10)); // 30

multiplier はラムダ式の外側で定義されていますが、ラムダ式の中から参照できています。

キャプチャは参照である

重要なのは、キャプチャされるのは変数の「値」ではなく「参照」だという点です。ラムダ式の実行時点での値が使われます。

int counter = 0;

Action increment = () => counter++;

increment();
increment();
increment();

Console.WriteLine(counter); // 3

ラムダ式の中で counter を変更すると、元の変数にも反映されます。

ループでの注意点

ループ内でラムダ式を作成するとき、キャプチャの挙動に注意が必要です。

var actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (var action in actions)
{
    action();
}
// 出力: 3, 3, 3(すべて同じ値!)

すべてのラムダ式が同じ変数 i をキャプチャしているため、ループ終了後の値(3)が出力されます。

ループでの正しい書き方

意図した動作にするには、ループ内でローカル変数にコピーします。

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();
}
// 出力: 0, 1, 2
変数を直接キャプチャ

ループ変数の最終値がすべてのラムダ式で共有される

ローカル変数にコピー

各イテレーションで独立した値がキャプチャされる

foreach では問題なし

C# 5.0 以降、foreach ではループ変数が各イテレーションで新しくなるため、この問題は発生しません。

var actions = new List<Action>();
var items = new[] { "A", "B", "C" };

foreach (var item in items)
{
    actions.Add(() => Console.WriteLine(item));
}

foreach (var action in actions)
{
    action();
}
// 出力: A, B, C

クロージャは強力な機能ですが、キャプチャの仕組みを理解していないと予期しない動作につながります。