Dart定数式の深淵:コンパイル時評価の全貌とVM内部構造
Dartの `const` は、単なる「変更不可の変数(immutable)」ではない。それは、Dartのコンパイラ(CFE: Common Front End)とDart VMの定数評価器(Constant Evaluator)に対し、「この式はプログラムの実行開始よりも前、すなわちコンパイル時に完全に解決されなければならない」という厳格な指令である。
シニアエンジニアやランタイムの挙動に関心を持つ開発者であれば、`const` が単なるシンタックスシュガーではなく、メモリレイアウトの最適化、Isolate起動時のオーバーヘッド削減、そしてオブジェクトグラフの共有(Canonicalization)を根底から支える極めて重要なプリミティブであることを知っているはずだ。
本稿では、Dartの定数式において何が許され、何が拒絶されるのか。その境界線をコンパイル時の挙動とVMのメモリモデルの観点から徹底的に解剖する。
—
1. コンパイル時評価のメカニズムと「定数式(Constant Expressions)」の定義
DartのコードがAOT(Ahead-Of-Time)コンパイルされる際、あるいはJITモードでVMにロードされる際、CFEはコードの構文木(AST)を走査し、`const` コンテキストで出現する式を評価する。
ここで重要なのは、「実行時(Runtime)の型や値に依存できない」という大原則である。すべてのオペランドは、コンパイル時に既知の定数でなければならない。
許容される基本演算と評価の限界
Dartの定数式では、以下の演算がコンパイル時に解決される。
- 算術演算: `+`, `-`, “, `~/`, `%`, `/`(整数の割り算は `~/` が必須。`/` は浮動小数点数を返すため、厳密な定数評価では型とオーバーフロー挙動に注意が必要)
- ビット演算: `&`, `|`, `^`, `~`, `<<`, `>>`, `>>>`
- 比較・論理演算: `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `!`
- 条件演算子(三項演算子): `cond ? expr1 : expr2`
- コレクションリテラル(`const []`, `const {}`, `const {}`(Map))
しかし、以下の演算や操作はコンパイルエラーとなる。
- メソッド呼び出し(`DateTime.now()` やカスタムメソッド。ただし、`const` コンストラクタの呼び出し、および特定のコアライブラリの定数メソッドを除く)
- 型キャスト(`as`)や型チェック(`is` / `is!`)のうち、実行時型情報に依存するもの(ただし、コンパイル時型に対する `is` は評価可能)
- 変数への代入(`=` や再代入)
// コンパイル時評価の境界線を示す実例
const int baseValue = 10;
// const int dynamicValue = baseValue + DateTime.now().millisecond; // ❌ コンパイルエラー: 実行時関数を定数式に含めることはできない
const int computedConst = baseValue 3 + (1 << 2); // ✔️ すべてコンパイル時既知の数値
---
2. Canonicalization(正準化)とイミュータブルオブジェクトのメモリ効率
Dart VMは、同一の `const` 式によって生成されるオブジェクトをメモリ上で完全に同一のインスタンスへと統合する。これを Canonicalization(正準化) と呼ぶ。
VM内部におけるオブジェクトの共有
通常、インスタンスを生成するたびにヒープ(Heap)上にメモリが割り当てられるが、`const` オブジェクトはコンパイル時に定数プール(Constant Pool)に配置され、Isolateのライフサイクル全体を通じて共有される。これにより、GC(ガベージコレクション)の圧力が劇的に軽減される。
class NetworkConfig {
final String host;
final int port;
const NetworkConfig({required this.host, required this.port});
}
void main() {
const configA = NetworkConfig(host: ‘api.dart.dev’, port: 443);
const configB = NetworkConfig(host: ‘api.dart.dev’, port: 443);
// 以下の比較は、内容が等しいだけでなく、メモリ上のアドレスが完全に一致する
print(identical(configA, configB)); // 圧倒的な true
}
この動作の背後では、CFEとVMのローダーがオブジェクトのフィールド値をハッシュ化し、定数テーブル内で一意性を保証している。セキュリティやメモリ管理の観点から言えば、不変な設定値やルーティングテーブルなどを `const` で定義することは、ヒープ汚染を防ぐための最も強力な防壁となる。
—
3. `const` コンストラクタの厳格な条件
カスタムクラスで `const` コンストラクタを定義するためには、以下の鉄則を満たさなければならない。満たさない場合、CFEは即座にコンパイルエラーを吐き出す。
1. すべてのインスタンス変数(フィールト)が `final` でなければならない。
2. コンストラクタの本体(Body)は空(`{}`)でなければならない。(ロジックを記述することは許されない)
3. 初期化リスト(Initializer list)内で実行される式も、すべて定数式でなければならない。
4. リダイレクトコンストラクタやスーパークラスの呼び出しも `const` でなければならない。
class SecureToken {
final String _entropy;
final int _createdTimestamp;
// constコンストラクタの定義
const SecureToken(this._entropy, this._createdTimestamp);
// 工場出荷時のデフォルト定数
const SecureToken.empty() : _entropy = ”, _createdTimestamp = 0;
// ❌ 以下の記述はコンパイルエラーになる
// SecureToken.invalid() {
// _entropy = ‘error’; // 本体にコードを書くことは許されない
// }
}
実行時評価とコンパイル時評価のハイブリッド(Const Context의 伝播)
`const` コンテキスト内では、通常のコンストラクタ呼び出しであっても `const` キーワードを付与することで、自動的に定数式として評価される。逆に、`const` コンテキスト外(通常の関数内)であっても、右辺が `const` であればコンパイル時定数として扱われる。
const defaultToken = SecureToken(‘initial_entropy’, 1704067200);
void processToken() {
// 実行時コード内であっても、constオブジェクトは定数プールから参照される
const localToken = SecureToken(‘local_entropy’, 1704067200);
// 以下の変数は通常の実行時インスタンス生成
final runtimeToken = SecureToken(‘runtime_entropy’, 1704067200);
print(identical(defaultToken, localToken)); // true (同一の定数プールエントリ)
print(identical(localToken, runtimeToken)); // false (ヒープ上に新規割り当て)
}
—
4. 高度な演算:コレクションとスプレッド構文の定数評価
Dart 2.12以降、コレクションLiteral(List, Set, Map)において、スプレッド演算子(`…`)や条件付きスプレッド(`…?`)、さらにはコレクションのif/for構文が `const` コンテキスト内でサポートされている。
これらは単なるシンタックスシュガーではなく、コンパイラが静的に要素を展開・評価し、最終的な不変コレクションを単一のイミュータブルなデータ構造として構築する。
const List
const bool includeExtra = true;
const List Dartにおける `const` と定数式の完全な理解は、単に「コードを綺麗にするため」「警告を消すため」のものではない。 1. メモリフットプリントの最小化: 重複するオブジェクトの排除により、モバイルアプリやサーバーサイドDart(Dart VM on Server)におけるRAM消費量を極限まで削減する。 Dartという言語の歯車がどのように噛み合い、コードがバイナリへと昇華していくか。その最前線にある「定数式」のルールを掌中に収めたとき、あなたの書くコードは、ただ動くだけのプログラムから、洗練されたランタイム・アートへと進化する。
0,
…baseList,
if (includeExtra) 4,
for (int i = 5; i <= 6; i++) i,
];
void main() {
// コンパイル時に展開され、[0, 1, 2, 3, 4, 5, 6] という単一の不変リストとしてVMにロードされる
print(optimizedList);
}
このコードにおいて、CFEは `if` や `for` のループをコンパイル時にシミュレートし、実行時には一切の条件分岐やループのオーバーヘッドを発生させないバイトコードを生成する。これが、Dartのメタプログラミングとパフォーマンスチューニングの真骨頂である。
---
5. チーフアーキテクトからの提言:定数式を極めることの意味
2. Isolate間のデータ共有: 厳密に定数化されたオブジェクトは、Isolate間でメッセージングを行う際、シリアライズのコストをゼロに近づけるポテンシャルを秘めている(将来的なVMの最適化において極めて有利)。
3. 予測可能な決定論(Determinism): 実行時の一品料理的な計算をコンパイル時に追放することで、バグの温床を断ち切る。