【入門編】Dartの変数宣言におけるスコープの理解と、クロージャ内での変数キャプチャの仕組み – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartの世界へようこそ。伝説的アーキテクトの私です。

今回は、Dartの変数宣言における「スコープ(有効範囲)」と、関数を深く理解する上で避けて通れない「クロージャの変数キャプチャ」について、徹底的に解き明かしていきます。

「他の言語から来たけれど、クロージャの中で変数を変なふうにいじってしまいバグらせた」「変数がいつメモリから消えるのかイマイチピンとこない」——そんなモヤモヤを、今日ここで完全にクリアにしましょう。ここをマスターすれば、あなたのDartコードの解像度は一気に跳ね上がりますよ。

—

1. 変数の寿命とスコープの基本:変数はどこで生まれ、どこで消えるのか?

まずは、変数が生きられる世界(スコープ)のお話です。
Dartのスコープは基本的に「レキシカルスコープ(静的スコープ)」を採用しています。これは、「コードを書いた時点で、どの変数がどこから見えるかが決まる」というルールです。

イメージとしては、ロシアの「マトリョーシカ」を思い浮かべてください。外側の大きな人形は内側を見渡せますが、内側の小さな人形から外側のプライベートな領域は直接見えません(外側が用意したものは見えますが)。

void main() {
// 【外側の世界】main関数のスコープ
String globalMessage = ‘こんにちは、Dart’;

if (true) {
// 【内側の世界】if文のブロック(スコープ)
String localMessage = ‘秘密のメッセージ’;

print(globalMessage); // ⭕️ 外側の変数は見える
print(localMessage); // ⭕️ 自分のスコープの変数だから見える
}

print(globalMessage); // ⭕️ 見える
// print(localMessage); // ❌ エラー!if文の外からは見えない・アクセスできない
}

メモリの寿命はどうなっているのか?

Dart VM(仮想マシン)の視点で見ると、変数が宣言されたブロック( `{}` )の実行が始まるとメモリ上に領域が確保され、そのブロックの実行が終了した瞬間に、その変数が占めていたメモリは回収(ガベージコレクションの対象)されます。

「ブロックを抜ければ、変数は消滅する」——これが基本原則です。

—

2. クロージャの魔術:消えるはずの変数が「キャプチャ」される

さて、ここからが本題であり、Dartが持つ美しくも強力な仕組みです。
「ブロックを抜けると変数は消える」と説明しましたが、例外があります。それがクロージャ(Closure)です。

クロージャとは、「自分の生まれた環境(スコープ)の変数を覚えたまま、別の場所へ持ち出せる関数」のことです。コード例を見てみましょう。

// カウンターを作る関数
Function createCounter() {
int count = 0; // createCounterのスコープにある変数

// この無名関数が「クロージャ」として外に持ち出される
return () {
count++; // 外側の変数 ‘count’ を操作している!
return count;
};
}

void main() {
// counter1 は専用の記憶装置(count)を持っている
var counter1 = createCounter();

print(counter1()); // 出力: 1
print(counter1()); // 出力: 2
print(counter1()); // 出力: 3

// まったく別の新しいカウンターを作る
var counter2 = createCounter();
print(counter2()); // 出力: 1 (counter1とは別のメモリを生きている)
}

なぜ `count` は消えないのか?(裏側の仕組み)

通常のルールなら、`createCounter()` の処理が終わった時点で、ローカル変数 `count` はメモリから消え去るはずです。

しかし、DartのコンパイラとVMは賢くできています。内部で返される無名関数(クロージャ)が `count` を参照している(キャプチャしている)検知すると、Dart VMは「おっと、この変数 `count` は関数が終了した後もまだ使われるぞ」と判断します。

結果として、`count` は通常のスタック領域ではなく、ヒープ領域(Heap)という安全な場所に退避させられ、クロージャと一緒に生き続けます。これが「変数キャプチャ」の正体です。

—

3. よくある罠:ループとクロージャのキャプチャ問題

初学者が一番ハマりやすいポイントを一つご紹介します。Flutterでリストビューやボタンのコールバックを動的に生成するときによく起きる現象です。

以下のコードを見て、出力がどうなるか予想できますか?

void main() {
var callbacks = [];

for (int i = 0; i < 3; i++) { // ループの中でクロージャを作る callbacks.add(() { print('値は: $i'); }); } // 作成した関数をすべて実行してみる for (var callback in callbacks) { callback(); } } 「おっ、`0`, `1`, `2` の順番で表示されるはず!」と思った方、惜しいです! Dart(および現代の多くの言語)における `for (int i = 0; ...)` のスコープの仕様では、`i` はループのたびに新しいインスタンスとして作り直されます(※JavaScriptの `let` と同様の挙動です)。 そのため、各クロージャは生成された瞬間の `i` のスナップショット(値)をしっかりとキャプチャするため、期待通りに以下のように出力されます。 値は: 0 値は: 1 値は: 2

【注意】もし変数がループの外で宣言されていたら?

もし、変数をループの「外」で回し回し使っていた場合、すべてのクロージャが「同じ一つの変数」を共有(キャプチャ)してしまい、意図しないバグを生みます。

void main() {
var callbacks = [];
int i = 0; // ループの外で宣言

for (i = 0; i < 3; i++) { callbacks.add(() { print('値は: $i'); }); } for (var callback in callbacks) { callback(); } } このコードの実行結果はこうなります。 値は: 3 値は: 3 値は: 3 解説: ループが終了した時点で `i` の値は `3` になっています。3つのクロージャはすべて「同じ `i` という変数の参照」を共有しているため、後から実行されたときには、すでに `3` に変わってしまった `i` を参照してしまうのです。

変数の寿命と、どこを指しているのか(参照か、値か)を意識するだけで、こうした不可解なバグとは一生おさらばできます。

—

まとめ

ここまでのポイントを綺麗にまとめておきましょう。

1. スコープと寿命:変数は宣言されたブロック `{}` の中で生まれ、そこを抜けると原則として消える。
2. クロージャのキャプチャ:関数が外側のスコープの変数を指している場合、Dart VMは自動的にその変数をヒープに退避させ、寿命を延命する。
3. 参照の共有に注意:複数のクロージャが同じ変数をキャプチャしている場合、変数の値が変化するとすべてのクロージャからその影響が見えてしまう。

ここをクリアすれば、Dartの非同期処理(Future / Stream)や、Flutterの複雑なウィジェット構築における状態管理のコードも、裏側で何が起きているのかが手に取るようにわかるようになります。

基本をマスターすれば、Dartはあなたにとって最高に心地よく、堅牢な相棒になりますよ。それでは、次のセッションでお会いしましょう!

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