はじめに
「関数が終わったのに、なぜ変数が消えないの?」
その謎を解く鍵が クロージャ(closure) にあります。
クロージャとは
クロージャ(closure) とは、
「関数」と「その関数が定義されたスコープの変数環境」をセットで保持する仕組みです。
つまり、「関数が外の変数を覚えている」状態。
簡単な例
Function makeCounter() {
int count = 0; // 外側の変数
return () {
count++;
print(count);
};
}
void main() {
var counter = makeCounter(); // makeCounter() はここで一度だけ呼ばれる
counter(); // => 1
counter(); // => 2
counter(); // => 3
}
ここでのポイント
-
countはmakeCounter()内で定義されているローカル変数。 - 通常なら
makeCounter()が終わるとcountは破棄されるはず。 - しかし
counter()が呼ばれるたびにcountは増えている。
関数終了後も変数が保持されている。
これがクロージャの力です。
クロージャの仕組み(内部動作)
① 定義時にスコープを“閉じ込める”
関数が定義されるとき、
その関数は 自分がアクセスできる変数(環境) を一緒に「捕まえ」ます。
int x = 10;
Function makeAdder(int n) {
return (int y) => x + n + y;
}
このとき、(int y) => x + n + y という関数は以下を記憶しています:
- グローバル変数
x - 引数
n(makeAdderのローカル変数)
これを 環境(Environment) と呼びます。
② 実行後もメモリ上に残る
void main() {
var add5 = makeAdder(5);
print(add5(3)); // => 18
}
makeAdder(5) が終わっても、
n = 5 の情報は破棄されません。
なぜなら、返された関数がその変数を参照しているからです。
この仕組みのために、Dart のランタイムは
n の値をヒープ(heap)領域に退避して保持します。
メモリの仕組み:スタックからヒープへ
通常、関数のローカル変数はスタック(stack) に保存され、
関数が終了すると破棄されます。
しかし、クロージャにキャプチャされた変数は破棄できません。
なぜなら、その関数が外でも使われる可能性があるから。
このためDart(や他の言語)では:
| 状況 | 保存場所 |
|---|---|
| 通常のローカル変数 | スタック(関数終了で破棄) |
| クロージャでキャプチャされた変数 | ヒープ(関数終了後も保持) |
イメージ図
✅
makeCounter()が終わってもcountは無名関数によって参照されているため解放されない。
メモリリークとガーベジコレクション(GC)
クロージャによる変数保持は非常に便利ですが、
参照が残り続けるとメモリリークの原因にもなります。
例えば:
List<Function> callbacks = [];
void registerCallback() {
int counter = 0;
callbacks.add(() {
counter++;
print(counter);
});
}
void main() {
for (int i = 0; i < 1000; i++) {
registerCallback();
}
// callbacks が残り続ける限り、1000個の counter がヒープに保持される
}
対策:
- 不要になったクロージャをリストから削除する
callbacks.clear()を呼び出してGCに開放させる
Dart のガーベジコレクタは、
参照が切れた時点でヒープ上の変数を自動的に解放します。
まとめ
| 項目 | 内容 |
|---|---|
| 定義 | クロージャは「関数+スコープ環境」を保持するオブジェクト |
| 特徴 | 外部関数が終了してもローカル変数を保持できる |
| メモリ動作 | キャプチャされた変数はヒープ領域に退避 |
| 用途 | カウンタ・状態保持・イベントハンドラなど |
| 注意点 | 不要な参照を残すとメモリリークの原因になる |
クロージャは、DartやJavaScriptにおける
「状態を持つ関数」を作るための最も重要な概念です。
レキシカルスコープに基づく環境キャプチャによって、
関数が実行を超えて変数を保持できるのです。