1. はじめに
プログラミングでクロージャ(closure)とは、JavaScriptのドアを閉める方法ではなく、ラムダ式や匿名メソッドが周囲のコンテキストから変数をキャプチャし、その変数が宣言されたブロックが終わった後でも「覚えている」仕組みのことだ。 簡単に言うと、クロージャは生まれたときの条件を覚えている関数で、小さなスーツケースに個人的なもの(変数)を入れて持ち歩いているようなイメージだ。
クロージャとは、その関数と、その関数が作られたときに存在していた環境(scope)を一緒に持つものだ。
最も単純なクロージャの例
実際に見てみよう:
Func<int> MakeCounter()
{
int count = 0;
return () =>
{
count++;
return count;
};
}
こう呼び出す:
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
どう動くのか?
- 変数 count はメソッド MakeCounter の中で宣言されている。
- ラムダ () => { ... } は外に返され、今やメソッドの外で生きている。
- しかし! ラムダは変数 count を覚えている。メソッド MakeCounter はもう終わっているのにだ。
これがクロージャだ:ラムダが周囲のコンテキストから変数 count を「閉じ込めた(キャプチャした)」のだ。
ラムダは何をキャプチャするのか?
- 周囲のメソッド(scope)からのローカル変数、
- メソッドのパラメータ、
- ブロック内の変数(for, foreach など)。
重要:変数は値でなく参照でキャプチャされる! クロージャの中で変数を変更すれば、それは「外」から見ても変わる。実際にはC#のコンパイラはこれらの変数用に特別な補助クラスを作るが、概念としてはこの点を覚えておけばクロージャを安心して使える。
2. クロージャとレキシカルスコープ
「関数ファクトリ」の例を拡張してみよう:
Func<int, int> PowerFactory(int power)
{
return x =>
{
int result = 1;
for (int i = 0; i < power; i++)
result *= x;
return result;
};
}
使い方:
var square = PowerFactory(2); // x^2
var cube = PowerFactory(3); // x^3
Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2)); // 8
関数 square と cube は異なる値の変数 power で作られ、それぞれ自分の値を覚えている。PowerFactory を呼ぶたびに「捕まえた値のリュック」がそれぞれ作られる。
3. キャプチャされた変数のミューテーション
時々疑問になるのは、ループ内で複数のラムダを作ってループ変数をキャプチャしたらどうなるか、という点だ。ここでよくハマる。
例:ループ内のクロージャ
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
action(); // ???
期待するのは? 0, 1, 2? 実際はこうなる:
3
3
3
なぜか? すべてのラムダは同じ変数 i を「参照」している。ループが終わる時点で i はもう 3 になっており、それがすべての Action に見える値だ。
修正案
各ラムダが「自分専用」の値をキャプチャするために、ループの本体で新しい変数を作る:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
action(); // 0 1 2
今や copy は各イテレーションごとの新しい変数で、クロージャはそれをキャプチャする。
4. 実務でのクロージャの活用
データ処理とコールバック
非同期処理や遅延実行(イベントハンドラ、フィルタ、タスクスケジューリング)で、クロージャはパラメータとロジックをまとめて「パッケージ化」するのに便利だ。例えば:
void ProcessList(List<int> list, int threshold)
{
var filtered = list.Where(x => x > threshold);
foreach (var item in filtered)
Console.WriteLine(item);
}
ここで Where の中のラムダは変数 threshold をキャプチャしている。
関数の「ファクトリ」作成
パラメータを渡して、そのパラメータを内包した関数を受け取る、というパターンはフィルタやソート比較、UIの反応設定などに便利だ。
状態管理
別クラスを作らずにちょっとした状態を保持したいときに便利:
Func<string, string> CreateGreeting()
{
string prefix = "Hello";
return name =>
{
return $"{prefix}, {name}!";
};
}
5. 便利な注意点
裏側:C#でクロージャはどう働くか
ラムダがキャプチャするものは、コンパイラによって補助クラスに変換される:変数はフィールドになり、ラムダはメソッドになる。だから変数の状態は呼び出し間で生き続ける。
各「ファクトリ」は小さなオブジェクトを生む。これは普通のことで、.NETはこうしたオブジェクトを効率的に管理し、必要なときにだけ割り当てる。
メモ:不要な大きなオブジェクトをキャプチャしない
大きなオブジェクト(例: UIフォーム)をクロージャで捕まえると、そのラムダが生きている限りそのオブジェクトは解放されない。典型的なメモリリークの原因は、重いコンテキストをキャプチャしたラムダでイベントにサブスクライブし、アンサブスクライブしないこと(+=/-=)だ。
クロージャとコレクションのライフサイクル — LINQの例
クロージャはLINQを柔軟にする:フィルタは自分のパラメータを覚える。
List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
Console.WriteLine(color); // "Blue"
その後で startsWith を変更すると、結果も変わる:
startsWith = "R";
foreach (var color in filtered)
Console.WriteLine(color); // "Red"
これは、クロージャが同じ変数 startsWith を参照しており、StartsWith が毎回現在の値をチェックするからだ。
6. クロージャでよくある間違い
ミス1: 使用時点までに変わる変数をキャプチャしてしまうこと。
ループの古典的なケース:クロージャ内のラムダが同じループ変数を見ていて、呼び出し時には既に別の値になっている。対処法はループ内で別の変数を作ることだ。
ミス2: 大きなコンテキストをまとめてキャプチャしてしまうこと。
クロージャがオブジェクト全体を引きずると依存が増え、コードが複雑になる。必要なものだけキャプチャするようにしよう。
ミス3: 重いリソースを保持してメモリリークを起こすこと。
ラムダで重いオブジェクトをキャプチャしてイベントにサブスクライブし、アンサブスクライブしないとオブジェクトは解放されない。サブスクライブのライフサイクルを管理し、必要なら明示的に -= すること。
ミス4: コードの管理性を失うこと。
クロージャを使いすぎるとデータソースや変更箇所の追跡が難しくなる。ロジックはできるだけ近くに置き、乱用を避けよう。
GO TO FULL VERSION