こんにちは!FlutterやDartを使った開発を楽しんでいますか?
今回は、Dartの根幹を支える極めて重要かつ深淵なテーマ、「定数式(Constant Expressions)と関数呼び出しの制限」についてお話しします。
他の言語、例えばJavaScriptやPythonなどからDartに入った開発者の方が、最初に「おや?」とつまずきやすいのが、このコンパイル時定数(`const`)の世界です。
「なんでこの関数は呼べないの?」「計算結果は同じなのに!」とイライラした経験はありませんか?
今回は、Dart VMの内部挙動やコンパイルの仕組みに少しだけ踏み込みながら、「なぜそれが制限されているのか」の技術的背景を、優しく紐解いていきましょう。ここをクリアすれば、Dartのコンパイルモデルの理解度がグッと跳ね上がりますよ!
—
1. そもそも `const` とは何をしているのか?(おさらい)
Dartにおける `const` は、単なる「書き換え不可(イミュータブル)」を意味する `final` とはまったく異なります。
`const` が付いた値やオブジェクトは、「アプリが実行される前(=コンパイル時)にその値が完全に確定していなければならない」という厳格なルールを持っています。
// これはOK:10 + 20 はコンパイル時に 30 と計算できるから
const int a = 10 + 20;
// これはNG! 実行時にならないとユーザーの入力や現在時刻はわからないから
const DateTime now = DateTime.now();
DartのAOT(Ahead-Of-Time)コンパイラやDart VMは、`const` で定義されたオブジェクトをバイナリ(機械語やスナップショット)のデータセグメントにあらかじめ焼き付けます。これにより、アプリ起動時のメモリ割り当てコストがゼロになり、極限まで最適化された高速な実行が可能になるのです。
—
2. なぜ「通常の関数呼び出し」は定数式に使えないのか?
さて、本題です。
次のようなコードを書いたとき、Dartのコンパイラはエラーを吐きます。
int square(int x) {
return x x;
}
// ❌ コンパイルエラーになる!
const int result = square(5);
「えっ、`square(5)` なんて、引数も中身も固定なんだから、コンパイル時に計算できるでしょ?」って思いますよね。なぜこれが許されないのでしょうか?
その理由は、「通常の関数は、Dart VMの実行時(Runtime)にスタックフレームを生成し、命令を実行するためのものだから」です。
Dart VMの視点から見た世界
Dartの関数は、第一級オブジェクトであり、クロージャを生成したり、副作用(外部の変数を書き換えたり、I/Oを行ったり)を持ったりする可能性があります。
もしコンパイラが「任意の関数」をコンパイル時に実行しようとすると、コンパイラの中にDartの実行時環境(VMのインタプリタやJITエンジン)そのものを内包して常時動かす必要が出てきます。
これはコンパイル速度を劇的に低下させ、ビルドツールチェーンを非常に重く複雑にしてしまいます。Dartチームは、「予測可能で超高速なコンパイル」を維持するため、任意の関数呼び出しをコンパイル時定数の評価からバッサリと排除したのです。
—
3. じゃあ、何なら計算できるの?(許可されている演算)
「じゃあ何も計算できないの?」というと、そんなことはありません。
Dartのコンパイラは、「副作用が一切なく、完全に決定論的(Deterministic)であること」が保証された基本的な演算子やビルトインの式に限り、コンパイル時に評価(Constant Folding)することを許可しています。
具体的には、以下のようなものは `const` の中で使えます。
- 算術演算子(`+`, `-`, “, `~/`, `%` など)
- 比較・論理演算子(`==`, `!=`, `<`, `>`, `&&`, `||` など)
- 条件演算子(三項演算子 `cond ? a : b`)
- 定数コンストラクタを持つクラスのインスタンス化
// これらはすべて「安全にコンパイル時評価できる」とコンパイラが知っている式です
const bool isAdult = (20 >= 18);
const String fullName = ‘Dart’ + ‘ Language’;
const double area = 3.14 10 10;
—
4. 例外と救済措置:`const` じゃない関数、あるいは `const` コンストラクタ
「どうしても複雑な初期化ロジックを定数っぽく扱いたい!」という場合はどうすればよいでしょうか?
① `final` を使う
コンパイル時(アプリ起動前)に決める必要が無く、「一度代 depois値したら二度と変えない」だけで十分な場合は、`final` と通常の関数を使いましょう。
// 実行時の最初のアクセス時などに評価される(遅延初期化など)
final int result = square(5);
② `const` コンストラクタを使う
もしカスタムクラスのインスタンスを `const` にしたい場合は、そのクラスに `const` コンストラクタ を定義し、中で使うフィールドもすべて `final` にする必要があります。
class Point {
final int x;
final int y;
// コンストラクタに const をつける
const Point(this.x, this.y);
// 演算を行うメソッドも、定数式として扱える工夫をする(例:加算結果を返す別コンストラクタなど)
const Point add(Point p) => Point(x + p.x, y + p.y);
}
// 完全にコンパイル時定数としてメモリに焼き付けられる
const Point p1 = Point(1, 2);
const Point p2 = Point(3, 4);
const Point p3 = p1.add(p2); // これはOK(定数コンストラクタを使ったインスタンスメソッドのシミュレーション)
このように、Dartが提供するルールを逆手に取ることで、安全かつ効率的なコードを書くことができます。
—
まとめ:ここをクリアすればDartの基本はバッチリ!
今回は、Dartの定数式における「関数呼び出しの制限」について、コンパイラとDart VMの裏側の事情を交えて解説しました。
- `const` はコンパイル時に値が確定し、バイナリに焼き付けられるもの。
- 任意の関数呼び出しを許可すると、コンパイラ内にVMの実行環境が必要になり重くなるため禁止されている。
- 計算させたいときは、安全な基本演算子を使うか、要件に応じて `final` や `const` コンストラクタを使い分ける。
この違いを頭の片隅に置いておくだけで、コンパイルエラーに直面したときも「あ、いまDart VMの実行前ステージでこれをやろうとしたから怒られたんだな」と、スッと原因が導き出せるようになりますよ。
Dartの厳格さは、私たちに「安全で爆速なアプリケーション」をプレゼントしてくれている証拠です。この知見を武器に、ぜひ明日の開発も楽しんでいきましょう!