1. クロージャとは何か?
プログラミングにおけるクロージャ(closure)は、外側のコンテキストから変数をキャプチャする関数です。簡単に言うと、ラムダ式や匿名メソッドが自分の外で宣言された変数を使うと、その関数はクロージャになります。作成された時点のその変数の値を「覚えている」ように振る舞います。
日常のアナロジー:
秘密のレシピを紙に書いて封筒に入れておいたと想像してみてください。紙自体がその後失われても(変数が直接アクセスできなくなっても)、封筒(ラムダ)を持っている人はまだレシピにアクセスできます。
シンプルな例:
int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42
ここで getX はクロージャです。外側で宣言された変数 x を使っていますから。
2. なぜ変数のキャプチャが重要か?
C# ではクロージャがあちこちで使われます:
- コレクションや LINQ クエリ内で
- イベントや非同期メソッドにパラメータを渡すために
- ループ内でイベントハンドラを作るとき
- 複数の呼び出し間で「コンテキスト」を保持するために
クロージャがなければ、多くの典型的な C# のパターンは不自然になったり使えなかったりします。
実用例の説明
リマインダーアプリを作っていると想像してください。ユーザーが複数のリマインダーを設定し、一定時間後にメッセージを出す必要があるとします。ハンドラにラムダを渡して「何をリマインドするか」を覚えさせる、これが変数のキャプチャの典型です。
3. C# はどうやって変数を実装しているか
内部的には C# はトリックを使います:ラムダが外部変数を使うと、コンパイラは自動的に補助クラス(display class)を作ります。キャプチャされた変数はすべてこのクラスのフィールドになります。
概念図はこんな感じ:
外側の変数 ──► DisplayClass
▲
│
クロージャ(ラムダ)
コードでのイラスト
裏側で起きていることは次のようになります:
int x = 5;
Func<int> f = () => x;
// コンパイラはおおよそ次のように変換する:
class DisplayClass
{
public int x;
public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;
だから、変数が定義されたスコープを抜けた後でも、クロージャはその変数の最新の値を参照できます。
4. 変数の値は「固定」されるか、それとも変わるか?
C# では変数は参照でキャプチャされます。つまり、ラムダが変数を使い、その変数が別の場所で変更されれば、ラムダは新しい値を見ます。
例:
int x = 10;
Func<int> getX = () => x;
x = 20;
Console.WriteLine(getX()); // 20、10 ではない!
学生はよく、getX() が常に 10 を返すと思いがちですが、実際にはラムダはまだ存在する変数を読み取り、その変数は変更可能です。
いつ値が実際に固定されるのか?
ループ内で新しいスコープが各反復ごとに作られる場合(例えば foreach のように)、各イテレーションで新しい変数が作られれば、ラムダはその時の値を「記憶」します。
5. 例:ループ内のクロージャ — よくある罠
よくあるミス
ループからそれぞれの番号を出力するデリゲート配列を作りたいとします:
Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
action();
プログラムは何を出力するでしょうか?
5
5
5
5
5
おや?なぜ 0,1,2,3,4 ではないのでしょうか?
理由:
ラムダは同じ変数 i をキャプチャしており、その変数はループの進行と共に変化します。デリゲートを後で呼ぶときには、i は既に 5 になっています。
正しい書き方
各イテレーションごとに別の変数を作成する必要があります:
Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
int index = i; // 各イテレーション用の新しい変数!
actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
action();
これでプログラムは次を出力します:
0
1
2
3
4
display class とどう関係するか?
最初のバージョンでは全てのデリゲートが同じフィールドに「ぶら下がる」ため、結果が同じになります。二番目のケースでは各イテレーション毎に新しいローカル変数が作られ、それぞれのデリゲートのためにユニークな DisplayClass(または少なくともユニークなフィールドのコピー)が生成されます。
6. 変数キャプチャの実用シナリオ
例 1: 「コンテキスト」を持つイベント処理
タスクのリストがあり、それぞれのタスクに「実行」ボタンのハンドラを紐付けるとします。ハンドラのラムダがどのタスクを処理するかを「覚えている」必要があります:
foreach (var task in tasks)
{
button.Click += (sender, e) => CompleteTask(task);
}
ここで task は各イテレーションでキャプチャされます。上のループの罠にハマらないよう、正しくスコープされていることを確認してください。
例 2: 非同期操作
クロージャは非同期ロジックにパラメータを渡すときによく使われます。例えば非同期タスク開始時に変数をローカルスロットに保存する:
for (int i = 0; i < 3; i++)
{
int index = i; // 必須!
Task.Run(() => Console.WriteLine($"Task #{index}"));
}
ローカル変数がないと、すべてのタスクが同じ番号を出力してしまい、普通は望んでいる結果ではありません。
例 3: LINQ クエリ
LINQ は外側の変数を考慮して要素をフィルタや変換するためにクロージャをよく使います。例えば:
string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));
ここで Where のラムダは prefix の値をキャプチャし、StartsWith を呼びます。
7. クロージャでの注意点、制限、典型的な間違い
ミスその1: ループ内で全デリゲートが同じ変数を使うこと。
ループ内で全デリゲートが同じ変数を参照すると、結果は予想と異なることが多いです。各デリゲートごとに新しいローカル変数を作って共通参照を避けてください。
ミスその2: メソッド外の変数をクロージャがキャプチャすること。
クロージャがクラスのフィールドやメソッド外で宣言された変数をキャプチャすると、その変数への参照が残り続け、ガベージコレクタがオブジェクトを解放できない可能性があります。これがメモリリークにつながることがあります。
ミスその3: 長寿命のデリゲートとクロージャ。
クロージャを持つデリゲートが長期間保持される(例えば static フィールドに保存される)と、そこから参照されている変数も予想より長くメモリに残ります。これが隠れたメモリリークやパフォーマンス問題の原因になります。
GO TO FULL VERSION