Dartの`final`と`const`の決定的な違い:ランタイム評価とコンパイル時評価の境界線
君たちのコードレビューでよく見かける光景だ。`var`、`final`、`const`が入り乱れ、その選択の意図が見えない。特に`final`と`const`。どちらも一度代入したら再代入できないという共通点から、安易に同じものと捉えがちだが、その認識は根本的に間違っている。
この二つのキーワードがコードの堅牢性、パフォーマンス、そして保守性にどれほどの影響を与えるか、その真髄を理解せずにDartを語ることはできない。本記事では、単なる代入タイミングの違いに留まらず、Dart VMがそのコードをどう解釈し、AOTコンパイラがどのように最適化を施すのか、その境界線を深く掘り下げていく。Webエンジニアとしてフロントエンド開発やコンポーネント設計、非同期API連携を行う君たちにとって、これはバグの起きない堅牢な設計、パフォーマンス上のボトルネック回避、そして何より『なぜそのコードを書くべきか』という思考の基盤となるだろう。
—
1. `final`: 実行時に一度だけ初期化される参照の不変性
`final`キーワードは、「一度だけ初期化される変数」を宣言する。重要なのは、その初期化が「実行時(ランタイム)」に行われる点だ。
Dart VMの視点から言えば、`final`で宣言された変数は、その初期化時にヒープメモリ上に新しいオブジェクトを生成し、そのオブジェクトへの参照を固定する。参照そのものは不変だが、参照先のオブジェクトがミュータブル(変更可能)であれば、その内部状態は後から変更されうる。これが`final`が提供する「浅い不変性」の本質だ。
実践的なユースケースとコード例
`final`は、以下のような状況で威力を発揮する。
- 実行時に計算される値や、APIレスポンスのように動的に決定されるデータを保持する場合。
- クラスのインスタンス変数として、一度インスタンスが生成されたら変更されないプロパティを表現する場合。
// finalの基本:実行時に値が決定され、参照が固定される
// DateTime.now()は、コードが実行される瞬間の時刻を生成するため、コンパイル時には確定できない。
final DateTime now = DateTime.now();
print(‘現在時刻: $now’);
// now = DateTime.now(); // エラー: final変数には再代入できません
// final Listの「浅い不変性」を理解する
final List
print(‘初期ロール: $userRoles’);
// 参照先のListオブジェクトの内部状態は変更可能
userRoles.add(‘viewer’);
print(‘追加後ロール: $userRoles’); // 出力: [admin, editor, viewer]
// userRoles = [‘guest’]; // エラー: final変数には再代入できません。参照自体は固定されている。
// ユースケース: APIレスポンスやオブジェクトの状態を保持するクラス
class UserProfile {
final String id;
final String name;
final List
// コンストラクタで全てのfinalフィールドを初期化する必要がある
UserProfile(this.id, this.name, this.permissions);
}
final user = UserProfile(‘u001’, ‘Alice’, [‘read’, ‘write’]);
// user.id = ‘u002’; // エラー: finalフィールドには再代入できません
// permissionsリストの参照はfinalだが、その中身は変更可能
user.permissions.add(‘delete’);
print(‘ユーザー ${user.name} の権限: ${user.permissions}’); // 出力: [read, write, delete]
// コードレビューの指摘例:
// 「この `user.permissions` は `final` だが、実行時に変更されうる。
// もしこのリストが完全に不変であるべきなら、`List.unmodifiable` を検討するか、
// コンストラクタで `const []` のような不変なリストを受け取る設計にすべきだ。
// `final` は参照の不変性であり、オブジェクトの不変性を保証しない。」
—
2. `const`: コンパイル時に決定され、完全に不変な定数
一方、`const`キーワードは、「コンパイル時に値が決定され、かつ完全に不変であることが保証される変数」を宣言する。ここでの鍵は「コンパイル時」と「完全に不変」という二つの概念だ。
AOTコンパイラは、`const`宣言された値が出現すると、その値をプログラムのバイナリデータ内に直接埋め込む。実行時には、そのデータへの参照が定数プールから提供される。同じ`const`値が複数回出現しても、VMはメモリ上に単一のインスタンスのみを生成し、それを共有する。
これにより、以下のような恩恵が得られる。
- メモリフットプリントの削減: 同じ値の重複インスタンスが生成されないため、メモリ使用量が抑制される。
- パフォーマンスの向上: オブジェクトの比較(`==`演算子)が、参照比較(ポインタ比較)だけで済むため非常に高速になる。また、AOTコンパイル時に値が完全に確定しているため、JITコンパイル時の動的な解決が不要となり、ランタイムオーバーヘッドが減少する。
- キャッシュの効率化: 不変であるため、一度計算されたハッシュ値などがキャッシュされやすく、データ構造の操作が最適化される。
実践的なユースケースとコード例
`const`は、以下のような状況でその真価を発揮する。
- アプリケーション全体で共有される設定値やハードコードされた定数。
- UIの静的な部品や、変更されないプロパティ。
- 完全に不変なコレクション(List, Map, Set)の生成。
// constの基本:コンパイル時に値が決定される
const String appName = ‘MyAwesomeApp’; // コンパイル時に値が確定
const int maxRetries = 3; // コンパイル時に値が確定
print(‘アプリ名: $appName, 最大リトライ数: $maxRetries’);
// const Listの「完全な不変性」
const List
print(‘サポートされるロケール: $supportedLocales’);
// supportedLocales.add(‘fr’); // エラー: Unsupported operation: add
// supportedLocales[0] = ‘en-US’; // エラー: Unsupported operation: []=
// constコレクションは完全に不変であり、要素の追加・変更はできない。
// constコンストラクタを持つクラスのインスタンス
class Color {
final int red;
final int green;
final int blue;
// constコンストラクタは、全てのフィールドがfinalであり、
// かつその初期化値がconstである場合にのみ宣言できる。
// これにより、Colorオブジェクト自体もコンパイル時定数として扱えるようになる。
const Color(this.red, this.green, this.blue);
}
const Color primaryColor = Color(255, 0, 0); // コンパイル時にインスタンスが生成され、不変
const Color secondaryColor = Color(0, 0, 255);
const Color anotherPrimaryColor = Color(255, 0, 0); // primaryColorと同じ値
// 同じ値を持つconstインスタンスは、メモリ上で同じオブジェクトを参照する
print(‘primaryColor == anotherPrimaryColor: ${primaryColor == anotherPrimaryColor}’); // 出力: true
// これは参照比較が成功していることを意味する。
// Flutterにおけるconstの重要性
// FlutterのウィジェットはStatefulWidgetの状態が変更されない限り、再構築される必要がない。
// `const`キーワードで宣言されたウィジェットは、フレームワークが「このウィジェットは不変である」と認識し、
// 親ウィジェットが再構築されても、自身の再構築をスキップする。
// これは描画パフォーマンスに絶大な影響を与える。
/
// 例: Flutterウィジェット
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// このTextはconstなので、親Widgetが再構築されても自身は再構築されない
const Text(‘Welcome to the app!’),
// このIconも同様
const Icon(Icons.star),
// これはconstではないので、親が再構築されるたびに新しいインスタンスが生成される可能性がある
Text(‘Current time: ${DateTime.now()}’), // 実行時に値が決定するためconstにはできない
],
);
}
}
/
// コードレビューの指摘例:
// 「なぜこのTextウィジェットに `const` をつけていない?テキスト内容は固定だろう。
// `const` をつけるだけで無駄な再構築を避け、アプリケーションのパフォーマンスを向上させることができる。
// Flutterでは、再構築コストの高いウィジェットツリーの中で、不変な部分を`const`でマークすることは極めて重要だ。」
—
3. `final`と`const`の決定的な違い:比較と実践的視点
両者の本質的な違いを明確にしよう。
| 特徴 | `final` | `const` |
| :————- | :————————————- | :————————————— |
| 初期化タイミング | 実行時 (Runtime) | コンパイル時 (Compile-time) |
| 不変性の範囲 | 変数の参照が不変。
参照先のオブジェクトはミュータブルな場合あり (浅い不変性) | 変数の参照と、参照先のオブジェクト自体が完全に不変 (深い不変性) |
| メモリ管理 | 実行時にヒープにインスタンスが生成される。
同じ値でも異なるオブジェクトになりうる | コンパイル時に定数プールに格納。
同じ値は単一のインスタンスを共有。
メモリ効率が良い |
| パフォーマンス | `const`に比べてオブジェクト比較などでオーバーヘッドが生じうる | オブジェクト比較が参照比較で済み高速。
AOTコンパイルの最適化対象 |
| ユースケース | APIレスポンス、計算結果、`DateTime.now()`などの実行時情報、一度だけ設定されるインスタンスプロパティ | 設定値、UIの静的要素、ハードコードされた不変データ、`const`コンストラクタを持つオブジェクト |
コードレビューの観点と設計パターン
コードレビューで私が求めるのは、「なぜそのキーワードを選んだのか」という明確な根拠だ。`final`と`const`の選択は、単なる好みではなく、設計意図、パフォーマンス、そしてコードの堅牢性に直結する。
- `final`の活用シーン:
- 非同期データのキャッシュ: APIから取得したデータを一度受け取ったら、その参照は固定したいが、データ構造内部は動的に変更される可能性がある場合。
// final Listの参照は固定だが、中身は変更可能
final List
- 一度だけ計算される複雑な値: 初期化時に一度だけ実行される計算結果を保持する場合。
// 複雑な計算結果をfinalで保持
final double calculatedValue = (sqrt(12345) log(987)).toPrecision(2);
print(‘計算結果: $calculatedValue’);
- `const`の活用シーン:
- アプリケーション全体で共有される設定: 環境変数やAPIキー、アプリケーション名など。
const String API_BASE_URL = ‘https://api.example.com/v1’;
const int DEFAULT_PAGE_SIZE = 20;
void fetchData() {
print(‘Fetching from $API_BASE_URL with page size $DEFAULT_PAGE_SIZE’);
// … API呼び出しロジック …
}
- UIの静的な部品: Flutterでは特に重要。パフォーマンスを最適化し、ウィジェットツリーの不必要な再構築を防ぐ。
// Flutterの例 (概念的なコード)
// const EdgeInsetsGeometry padding = EdgeInsets.all(8.0);
// const BorderRadiusGeometry borderRadius = BorderRadius.circular(10.0);
// const Duration animationDuration = Duration(milliseconds: 300);
- ハードコードされた列挙値やステータスコード: プログラムのロジックで不変の識別子として使用する場合。
class HttpStatusCode {
static const int OK = 200;
static const int NOT_FOUND = 404;
static const int INTERNAL_SERVER_ERROR = 500;
}
void handleResponse(int statusCode) {
if (statusCode == HttpStatusCode.OK) {
print(‘Success!’);
} else if (statusCode == HttpStatusCode.NOT_FOUND) {
print(‘Resource not found.’);
}
}
—
4. 落とし穴と注意点
4.1. `final`コレクションの「浅い不変性」の誤解
最もよく見られる間違いの一つが、`final`なコレクションを完全に不変だと誤解することだ。
// finalコレクションの落とし穴
final List
print(‘初期リスト: $mutableList’);
mutableList.add(4); // OK: リストの参照はfinalだが、リストの中身は変更可能
print(‘変更後リスト: $mutableList’); // 出力: [1, 2, 3, 4]
// 本当に不変なリストが必要な場合
// 1. constを使う (コンパイル時定数の場合)
const List
// immutableCompileTimeList.add(40); // エラー: Unsupported operation: add
// 2. List.unmodifiableを使う (実行時定数の場合)
// APIレスポンスなど、実行時に値が決定されるが不変にしたい場合に有用
final List
// immutableRuntimeList.add(400); // エラー: Unsupported operation: add
print(‘不変な実行時リスト: $immutableRuntimeList’);
このように、実行時に生成されるリストやマップを完全に不変にしたい場合は、`List.unmodifiable`や`Map.unmodifiable`コンストラクタを利用する必要がある。
4.2. `const`が使える場所で`final`を使うことの損失
コンパイル時に値が確定し、不変であるべき場所で誤って`final`を使うと、潜在的なパフォーマンスとメモリの最適化を逃すことになる。
// 良くない例:コンパイル時定数なのにfinalを使っている
final String appVersion = ‘1.0.0’; // これはconstにすべき
final List
// なぜ良くないか?
// 1. appVersionが複数箇所で使われる場合、それぞれ別々のStringインスタンスがヒープに生成されうる。
// constなら単一のインスタンスが共有される。
// 2. defaultHeadersは実行時に新しいListオブジェクトが生成される。
// constなら定数プールから共有され、要素の変更もコンパイルエラーで防げる。
// 3. Flutterウィジェットなら、`const`を付けないことで無駄な再構築が発生しうる。
常に「この値はコンパイル時に決定されるか?」「この値は実行中に変更される可能性があるか?」を自問自答し、可能な限り`const`を使う習慣をつけよう。それはコードの意図を明確にし、コンパイラに最適なコード生成を促す、まさにプロフェッショナルの仕事だ。
—
まとめ
`final`と`const`、これら二つのキーワードは、単なる変数宣言のバリエーションではない。これらはDart言語が提供する強力な設計ツールであり、開発者の意図をコンパイラとVMに明確に伝えるための手段だ。
- `final`: 実行時に一度だけ初期化され、その参照が不変であることを保証する。参照先のオブジェクトがミュータブルであれば、その中身は変更されうる(浅い不変性)。
- `const`: コンパイル時に値が決定され、かつ値そのものが完全に不変であることを保証する(深い不変性)。同じ値は単一のインスタンスとしてメモリ上に配置され、共有されるため、パフォーマンスとメモリ効率に優れる。
君たちのコードが、単に動くだけの代物ではなく、堅牢で、高性能で、そして将来にわたって保守しやすい「美しいプロダクションコード」であるために、この境界線を深く理解し、適切に使いこなすことを強く求める。それは、君たちがDartを真に掌握した証となるだろう。