こんにちは!FlutterやDartの世界へようこそ。チーフアーキテクトの私です。
今回は、Dartの根幹を支える極めて重要かつ強力な機能、「定数式(Constant Expressions)」について深く掘り下げていきます。
他の言語(例えばJavaScriptやSwift、Kotlinなど)からDartにやってきた開発者が、最初に感動し、そして少しだけ混乱するのが、Dartの `const` が持つ圧倒的な「コンパイル時評価」の世界です。
「`const` って、値を変えられないだけでしょ? `final` と何が違うの?」
そんな疑問を持っているとしたら、ここから先の解説は目からウロコになるはずです。ここをクリアすれば、Dartのメモリモデルと最適化の哲学が手に取るようにわかるようになりますよ。
—
1. なぜ `const` は特別なのか?(変数の固定 vs メモリの固定)
まず、基本のおさらいからいきましょう。Dartにおける変数宣言には主に `var`、`final`、`const` の3つがありますよね。
- `var`: 値を後から変更できる(再代入可能)。型は推論される。
- `final`: 一度だけ値を代入できる(実行時イミュータブル)。
- `const`: 「完全な定数」。コンパイル時に値が完全に確定していなければならない。
イメージ図で考えてみましょう。
[ 実行時 (Runtime) ]
var –> [ 箱 ] (中身を入れ替え可能)
final –> [ 鍵付きの箱 ] (一度入れたら二度と変えられない。ただし中身を入れるのは「動いてから」)
[ コンパイル時 (Compile-time) ]
const –> [ 彫刻 (石像) ] (アプリが起動する「前」のビルド時点で形が決まり、バイナリに焼き付けられる)
`const` の最大の特徴は、「プログラムが動き出す前(コンパイル時)に、Dart VMの定数評価器によって計算が完了している」という点です。つまり、アプリが起動した瞬間には、すでにその計算結果やオブジェクトがメモリの特定の位置に「存在している」状態になります。
—
2. 定数式(Constant Expressions)で何ができるのか?
「コンパイル時に計算が終わっている」ということは、何でもかんでも書けるわけではありません。例えば、現在の時刻を取得する `DateTime.now()` は、ビルドする瞬間とユーザーがアプリを開く瞬間で時間が違うため、コンパイル時には確定できませんよね。
では、`const` の世界(定数式)では、具体的にどのような演算が許されているのでしょうか? その全貌を整理しましょう。
許容される基本演算
1. 算術演算: `+`, `-`, “, `~/` (整数除算), `%`
2. 比較・論理演算: `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `!`
3. ビット演算: `&`, `|`, `^`, `~`, `<<`, `>>`
4. 条件演算子(三項演算子): `condition ? trueVal : falseVal`
5. コレクション関連: const リスト、const マップ、const セットの構築、およびスプレッド演算子(`…`)
実際のコードで見てみましょう。
// コンパイル時定数の世界
const int baseSize = 10;
const int doubleSize = baseSize 2; // 算術演算 OK
const bool isLarge = doubleSize > 15; // 比較演算 OK
const String label = isLarge ? ‘Large’ : ‘Small’; // 三項演算子 OK
const List
const List
これらのコードはすべて、Dartのコンパイラがビルド時に計算を済ませ、単なる「結果の値」に置き換えます。実行時にCPUが計算するコストはゼロです。これがDartのパフォーマンスを極限まで引き上げる秘密の一つです。
—
3. 陥りやすい文法エラーと「コンパイル時」の壁
初学者が必ずと言っていいほどハマるポイントがあります。それは、「一見すると定数に見えるのに、エラーになるケース」です。
以下のコードを見てください。どこがエラーになるかわかりますか?
void main() {
const int a = 5;
// 1. これはエラーになりますか?
const int b = a + 10;
// 2. これはエラーになりますか?
final int runtimeValue = 10;
// const int c = runtimeValue + 5; // ❌ コンパイルエラー!
// 3. これはエラーになりますか?
// const String currentTime = DateTime.now().toString(); // ❌ コンパイルエラー!
}
解説
- 1番は OK です。 `a` 自体が `const` でコンパイル時に確定しているため、それに定数を足した `b` もコンパイル時に計算可能です。
- 2番は ❌ NG(コンパイルエラー)です。 `runtimeValue` は `final` ですが、これは「実行時に一度だけ代入される」変数です。コンパイル時には値が決まっていないため、`const` の右辺には使えません。
- ここが重要! `const` の中では `final` 変数ですら使うことができません。
- 3番も ❌ NG です。 `DateTime.now()` は実行時の環境に依存するため、コンパイル時には評価できません。
このように、定数式の世界に「実行時のゆらぎ」が1ミリでも混入すると、Dartのコンパイラは容赦なくエラーを投げます。
—
4. `const` コンストラクタの本質
クラスのインスタンスを `const` で生成したいと思ったことはありませんか? FlutterのUI構築(`const SizedBox(height: 16)` など)で頻繁に見かけるあれです。
クラスを `const` にするためには、以下の条件を満たす必要があります。
1. すべてのインスタンス変数(フィールド)が `final` であること。
2. コンストラクタに `const` キーワードが付与されており、処理の中身(ボディ)が空(セミコロン `;` のみ)であること。
class Point {
final int x;
final int y;
// const コンストラクタ
const Point(this.x, this.y);
}
void main() {
// コンパイル時にメモリ上に生成される
const p1 = Point(1, 2);
const p2 = Point(1, 2);
// Dartの最適化マジック:
// p1 と p2 は、メモリ上の「同一のインスタンス」を指します!
print(identical(p1, p2)); // true が出力されます
}
なぜ `identical(p1, p2)` が `true` になるのか?
Dart VMは、完全に同じ `const` オブジェクト(同じクラス、同じ引数)が複数コード内に現れた場合、メモリ上にいくつもオブジェクトを作らず、一つのインスタンスを使い回す(Canonicalization / 正規化)という最適化を行います。
これにより、メモリ消費量が劇的に減り、FlutterのようなUIフレームワークでは「無駄なウィジェットの再生成とそれに伴うGC(ガベージコレクション)の負荷」を完全に回避できるのです。
—
まとめ:ここをクリアすればDartはあなたのもの
いかがでしたでしょうか?
- `const` は単なる定数ではなく、「コンパイル時に評価され、バイナリに焼き付けられる値やオブジェクト」であること。
- 定数式の中で使えるのは、実行時に依存しない純粋な演算と、他の `const` 値のみ。
- `const` コンストラクタを使うことで、同一オブジェクトの共有(メモリ最適化)の恩恵を受けられること。
このあたりのメモリモデルやコンパイルの挙動が頭に入ると、Flutterでのパフォーマンスチューニングの見え方もガラリと変わってきます。
「なぜここでエラーになるのか?」と迷ったときは、「これ、アプリを作る前のパソコンの中(コンパイル時)だけで計算が完結できるかな?」と自分に問いかけてみてください。
ここをクリアできれば、あなたのDartの基礎力は間違いなくプロフェッショナルな領域に到達しています。明日からのコーディングを、さらに自信を持って楽しんでいきましょう!