Dartの真髄:`final`と`const`のランタイム境界とメモリ最適化の深層
Dart VMのコアコミッターとして日々コードベースを眺めていると、`var`、`final`、`const`の使い分けが単なる「スタイルの好み」や「IDEの警告を消すための儀式」として片付けられている現場に遭遇する。これは言語の仕様に対する深刻な誤解であり、アプリケーションのメモリフットプリント、起動時のレイテンシ、さらにはセキュリティ境界の設計において致命的な見落としを生む温床となる。
本稿では、`final`と`const`を分かつ評価タイミング(Evaluation Timing)の物理的境界をコンパイラとDart VMの挙動から解き明かし、シニアエンジニアが現場で迷わず最適な選択を下すための判断基準を提示する。
—
1. コンパイル時評価 vs ランタイム評価:根本的な違い
まず、Dartの型推論や変数のイミュータビリティという表層的な議論を一旦忘れ、「いつ、どこでその値が確定し、メモリ上にどう配置されるか」という低レイヤの事実に向き合おう。
`final`:ランタイムの不変性(Runtime Immutability)
`final`修飾子は、「一度だけ代入可能であること」を保証する。
コンパイラは、`final`変数が初期化される式が「いつ評価されるか」を強制しない。つまり、評価はプログラムの実行時(Runtime)に行われる。
final DateTime executionTime = DateTime.now();
// DateTime.now() はランタイムにOSのシステムコールを発行するため、コンパイル時には確定しない。
Dart VMの視点から見ると、`final`変数がバインドされる参照先は、ヒープメモリ(Heap Memory)上、あるいはスタックフレーム内に確保され、そのポインタへの書き込みが一度きりに制限されるだけである。
`const`:コンパイル時定数(Compile-time Constants)とカノニカル化
一方、`const`は「コンパイル時定数」である。値はAOT(Ahead-Of-Time)コンパイル時、あるいはJITのバイトコード生成時に完全に解決されなければならない。
const int timeoutLimit = 30 1000;
// 30 1000 はコンパイラ(CFE: Common Front End)によって即座に `30000` というリテラルに折り畳まれる(Constant Folding)。
さらに重要なのは、`const`オブジェクトはカノニカル化(Canonicalization)されるという点だ。同一の内容を持つ`const`オブジェクトは、Dart VMの定数プール(Constant Pool)内で単一のインスタンスを共有する。これにより、無駄なオブジェクトアロケーションが完全に排除され、GC(ガベージコレクション)のプレッシャーが劇的に軽減される。
—
2. メモリ最適化とIsolate間通信における「深さ」の差
マルチスレッドモデルの代わりにIsolateを採用するDartにおいて、イミュータビリティの「深さ(Deep immutability)」はセキュリティとパフォーマンスの要である。
ここでも`final`と`const`の挙動は大きく異なる。
- `final`の不変性: 変数バインドがイミュータブルであるだけで、指し示すオブジェクトの中身がミュータブルであれば、その状態は変更可能である(Transitiveではない)。
- `const`の不変性: 完全な「深さの不変性(Deep Immutability)」を強制する。内部のリストやマップに至るまで、すべてがコンパイル時定数で構築されていなければならない。
void examineIsolatePayload() {
// finalであっても、内部のListはミュータブル
final List
mutableListFinal.add(4); // 実行可能。変数自体はfinalだがリスト自体は書き換え可能
// constは再帰的にイミュータブルであり、Isolate間転送(SendPort)においてコピーコストが発生しない
const List
// canonicalizedConst.add(4); // コンパイルエラー (Unsupported operation)
}
Isolate間でデータをやり取りする際、`const`で定義されたデータ構造は実質的にリードオンリーのメモリ領域を共有できるため、シリアライズ/デシリアライズのオーバーヘッドをゼロに近づけることができる。
—
3. `final`と`const`を使い分けるための評価フローチャート
実際の設計フェーズにおいて、どちらを選択すべきかを決定するためのアルゴリズムを以下に示す。このフローチャートを脳内にインプットしてほしい。
[変数の定義が必要]
│
▼
【Q1: 値は「コンパイル時」に完全に確定しているか?】
(例: リテラル、数学的計算、他のconst参照のみで構成されているか?)
├── YES ──► 【Q2: オブジェクトやコレクションの構造も含めて、完全に不変(Deep Const)か?】
│ ├── YES ──► 【 const 】 を選択(カノニカル化の恩恵を受ける)
│ └── NO ──► (構造的にconstにできないケースは稀だが、型定義を確認)
│
└── NO ──► 【Q3: 値の確定にランタイムの情報が必要か?】
(例: DateTime.now(), I/O結果, ユーザー入力, 依存性注入のインスタンス)
├── YES ──► 【 final 】 を選択
└── NO ──► (もし実行時に関数等で一度だけ計算されるイミュータブルな値なら)
└── 【 final 】 を選択
実践的コード例:アンチパターンと最適解
以下のコードを見てほしい。一見何気ないウィジェットの定数定義だが、コンパイラの裏側で何が起きているかを意識しているだろうか。
class SecurityAuditor {
// 【アンチパターン】
// 毎回インスタンスが生成され、ヒープを圧迫する。ランタイム評価の必要性はない。
final List
// 【正しいアプローチ】
// コンパイル時に定数プールに焼き付けられ、アプリケーション全体で単一のインスタンスを共有する。
static const List
// 【ランタイム評価が必須のケース】
// ネットワークインターフェースから動的に取得する場合や、環境変数に依存する場合。
final String currentRegion;
SecurityAuditor(this.currentRegion);
}
`restrictedIpsFinal`をインスタンス変数として定義した場合、このクラスがインスタンス化されるたびにリストオブジェクトがヒープ上にアロケーションされる。一方、`static const`であれば、メモリ上の配置はバイナリに直結し、アロケーションコストは文字通り「ゼロ」になる。
—
4. チーフアーキテクトからの最終提言
Dartコードを書く際、`var`に手を伸ばす前に自問自答してほしい。
1. 「これは定数か?」 → 迷わず `const` を使え。コンパイラに仕事をさせ、バイナリサイズとメモリを最適化しろ。
2. 「これは初期化後に書き換える必要がないか?」 → `final` を使え。意図しない副作用を型システムとコンパイラによって排除しろ。
3. `var`はいつ使うのか? → ループカウンターや、アルゴリズムの途中で状態がミュータブルに変化することが強制される極めて限定的なスコープに留めよ。
コードの意図を厳密にコンパイラに伝えることは、単なるコードクリーンの範疇を超え、Dart VMのランタイム性能を極限まで引き出すための唯一無二の防壁となる。型安全とメモリ効率の境界線を支配する者こそが、真のDartエンジニアである。