【入門編】Dartの変数スコープとクロージャ:変数のキャプチャがメモリに与える影響 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartでの開発、日々の試行錯誤お疲れ様です。
新しい言語を学ぶとき、「変数宣言の使い分けは分かったけれど、裏側でメモリがどう動いているのか」気になったことはありませんか?

今回は、Dartの変数スコープと、少し一歩進んだ「クロージャによる変数のキャプチャ」がメモリに与える影響について、本質から紐解いていきましょう。ここをクリアすれば、Dartのメモリ管理の仕組みがグッとクリアになり、意図しないメモリリークを防ぐプロフェッショナルなコードが書けるようになりますよ。

—

1. 変数スコープの基本と「クロージャ」の正体

まずは、Dartの変数がどこまで生き残るのか(スコープ)と、クロージャの基本を確認しておきましょう。

Dartでは、中括弧 `{}` が現れるたびに新しい「スコープ(変数の有効範囲)」が生まれます。

void main() {
var globalMessage = “こんにちは”; // main関数のスコープ

if (true) {
var localMessage = “世界”; // if文のスコープ
print(globalMessage); // 外部の変数は読める
}

// print(localMessage); // ❌ エラー!if文の外からは見えない
}

ここまでは他の言語と一緒ですよね。では、「関数が作られたときの環境(スコープ)をそのままカプセル化して持ち運ぶ仕組み」であるクロージャを見てみましょう。

Function createCounter() {
int count = 0; // createCounterのローカル変数

// この無名関数が「クロージャ」
return () {
count++;
print(count);
};
}

void main() {
var counter = createCounter();
counter(); // 出力: 1
counter(); // 出力: 2
}

あれ? `createCounter()` という関数はすでに実行が終わって消滅しているはずなのに、返された `counter` を実行すると、内部の `count` が保持されて増えていっていますよね。

なぜ、消滅したはずの関数の変数にアクセスできるのでしょうか?

—

2. 変数の「キャプチャ」がメモリに与える影響

ここで、Dart VM(仮想マシン)やAOTコンパイラの内部で何が起きているのかを覗いてみましょう。

通常、ローカル変数は関数の実行が終わると同時に、メモリ上の「コールスタック」から綺麗に消え去ります。しかし、クロージャがそのローカル変数を参照している(キャプチャしている)場合、Dartのコンパイラとランタイムは賢く判断します。

> 「おっと、この変数 `count` はクロージャに後から使われるから、スタックから消すわけにはいかないぞ。ヒープ領域(Context)に退避させておこう」

頭の中で次のようなイメージ図を描いてみてください。

[ 通常のローカル変数 ]
main() ──> スタック領域 ──> 関数終了時に即消滅 💥

[ クロージャによるキャプチャ ]
createCounter() ──> ヒープ領域 (Context) ──> クロージャが参照し続ける限り生存 🛡️

つまり、クロージャが変数をキャプチャした瞬間、その変数の寿命は「関数自体の寿命」から「そのクロージャ(関数オブジェクト)がガベージコレクションされるまでの寿命」へと引き伸ばされるのです。

—

3. 陥りがちな罠:意図しないメモリリークと巨大なオブジェの保持

この仕組みを知らないと、実務で思わぬパフォーマンス低下やメモリリークを引き起こします。よくあるアンチパターンを見てみましょう。

🚨 ありがちな失敗例

Flutterのウィジェットや、画面ごとの大きなデータを扱うクラスの中で、非同期処理やイベントリスナーを登録する時によく起きます。

class HeavyDataScreen {
// 非常に重いデータ(画像リストや巨大なJSONなど)
final List hugeData = List.generate(1000000, (index) => ‘Data $index’);

void setupListener(Stream eventStream) {
// イベントが流れてきた時に何かをするリスナーを登録
eventStream.listen((_) {
// ⚠️ ここに注目!
// 「何気なく this や hugeData を直接使っていないつもりでも…」
print(“イベントを受信しました”);
});
}
}

「あれ? 上のコード、`hugeData` をクロージャの中で使ってないから大丈夫じゃない?」と思ったそこのあなた、鋭いですね!
しかし、Dart(およびJavaScriptなどの言語)の多くでは、インスタンスメソッド内でメンバー変数(`hugeData`)にアクセスする際、暗黙的に `this` をキャプチャしています。

もし `setupListener` 内のクロージャが何らかの拍子に `this`(画面のインスタンス)への参照を保持し続けてしまうと、「画面はすでに閉じてユーザーが見ていないのに、巨大な `hugeData` がメモリ上に居座り続ける」というメモリリークが発生します。

💡 対策:必要なものだけをローカル変数に切り出す

不要なオブジェクト全体のキャプチャを防ぐためには、クロージャに渡すスコープを最小限に絞ります。

class GoodDataScreen {
final List hugeData = List.generate(1000000, (index) => ‘Data $index’);

void setupListener(Stream eventStream) {
// もし必要なデータやフラグがあるなら、
// あらかじめプリミティブな値や必要な部分だけをローカル変数に切り出す
final bool isEnabled = true;

eventStream.listen((_) {
// hugeData や this をキャプチャせず、必要最小限のローカル変数だけをキャプチャさせる
if (isEnabled) {
print(“処理を実行”);
}
});
}
}

また、Flutterでよく使う `StreamSubscription` や `ChangeNotifier` などのリスナーは、画面が破棄されるタイミング(`dispose`時など)で必ず `cancel()` や `removeListener()` を呼び出し、クロージャへの参照そのものを断ち切ることが鉄則です。

—

まとめ:ここをクリアすればDartの基本はバッチリ!

  • クロージャは環境を覚えている:関数が終了しても、クロージャが変数をキャプチャしていると、その変数はヒープ領域に生き残り続けます。
  • メモリ保持のコストを意識する:便利な反面、意図せず巨大なオブジェクトや `this` をキャプチャし続けると、メモリリークの原因になります。
  • スコープを美しく保つ:必要のない大きなデータはキャプチャさせず、スコープを狭くクリーンに保つことが、洗練されたDartコードへの第一歩です。

仕組みの本質を知っていれば、エラーに慌てることも、メモリリークの闇に悩まされることもなくなります。ぜひ、日々のコードを書くときに「今、このクロージャは何をキャプチャしているかな?」と頭の片隅で意識してみてくださいね。

あなたのDart / Flutter開発ライフを、これからも応援しています!

タイトルとURLをコピーしました