こんにちは!FlutterやDartを使った開発を楽しんでいますか?
他の言語からDartに入ってきた開発者の方から、非常によく受ける質問があります。
それは「`final`と`const`って、どっちも一度代入したら変えられない定数だよね? 何が違うの?」という疑問です。
確かに、どちらも「再代入を許さない」という点では同じです。しかし、DartのコンパイラやVM(仮想マシン)の挙動という「内部の世界」を覗いてみると、この2つは全く異なるアプローチでメモリを支配しています。
ここをクリアすれば、Dartのメモリ管理やパフォーマンスチューニングの基本はバッチリマスターできますよ。今日は、私がコードの裏側の世界までご案内しますね。
—
1. ざっくり掴む:`final` と `const` の決定的な違い
まずは、直感的なイメージ図から入りましょう。
- `final`(実行時定数)
- イメージ:「鍵付きのロッカー」
- アプリが実行されている最中(Runtime)に、中身が一度だけ決まります。決まったらもう鍵をかけて開けられません。
- `const`(コンパイル時定数)
- イメージ:「最初から石に刻まれた神聖な文字」
- アプリがビルドされる瞬間(Compile time)には完全に中身が決まっています。実行される前、コードを書いている時点で既にメモリの定まった場所に存在します。
—
2. コードで見る挙動の違いとコンパイルの裏側
百聞は一見に如かず。実際にコードを書いてその違いを確認してみましょう。
void main() {
// 1. 現在時刻を取得する関数
DateTime now = DateTime.now();
// 2. finalならOK(実行時に値が決まるから)
final currentFinalTime = now;
// 3. constだとここでコンパイルエラーになる!
// const currentConstTime = now;
// 理由: now()の結果は「アプリを動かしてみないと分からない(実行時)」ため、
// コンパイル時には値が確定できないからです。
}
なぜ `const` は `DateTime.now()` を拒絶するのか?
`const` を使いたい場合、Dartのコンパイラは「コードをビルドするその瞬間に、値が完全に予測可能であること」を要求します。
`DateTime.now()` はプログラムを実行した瞬間にOSから時刻をもらうため、コンパイル時には値が決まりません。そのため、`const` で縛ろうとするとコンパイラに怒られてしまうのです。
—
3. メモリ最適化への影響:なぜ `const` は「神」なのか?
ここからがDartの真骨頂です。プログラミング初学者の方こそ、このメモリの仕組みを知っておくと将来的に大きな差がつきます。
Dart VMは、`const` で定義されたオブジェクトに対して「カノニカル化(Canonicalization)」という強力な最適化を行います。言葉は難しいですが、要するに「同じ内容の `const` オブジェクトは、メモリ上に一つだけ作って、みんなで使い回す」という仕組みです。
以下のコードを見てください。
class Point {
final int x;
final int y;
const Point(this.x, this.y);
}
void main() {
// パターンA: constでインスタンス化
const p1 = Point(1, 2);
const p2 = Point(1, 2);
// パターンB: 通常のコンストラクタ(あるいはfinal)でインスタンス化
// final p3 = Point(1, 2);
// final p4 = Point(1, 2);
// p1とp2はメモリ上の「全く同じ場所」を指しているか?
print(identical(p1, p2)); // 答えは true!
}
`identical()`関数は、2つの変数がメモリ上の全く同一のアドレスを指しているかを厳密に調べる関数です。
`const` を使った場合、`p1` と `p2` は別々の場所で作られるのではなく、Dart VMによってメモリ上の同じ領域を共有させられます。
これが何を意味するか分かりますか?
数千、数万個のウィジェットやデータを扱うFlutterアプリにおいて、構造が同じ `const` オブジェクトを適切に使い分けるだけで、不要なオブジェクト生成によるガベージコレクション(GC)の嵐を防ぎ、メモリ消費量を劇的に削減できるのです。
—
4. 陥りやすい文法エラー:「深さ」の罠
`const` を使うとき、初学者が最もハマりやすい罠が「深い不変性(Deep Immutability)」です。
void main() {
// constでリストを定義
const names = [‘Alice’, ‘Bob’];
// 実行時エラーが発生する!
// names.add(‘Charlie’);
// UnsupportedError: Cannot add to an unmodifiable list
}
`const` をコレクション(リストやマップ)に使うと、その中身の要素も含めて完全に凍結(カンペキなイミュータブル)されます。後から要素を追加・変更することは一切できません。
ここでよくある誤解が、`final` リストとの違いです。
void main() {
// finalのリスト
final names = [‘Alice’, ‘Bob’];
// これはOK!リスト自体の「中身」は変更できる
names.add(‘Charlie’);
// エラー:ただし、リストそのものを別の箱にすり替えることはできない
// names = [‘David’]; // Compile Error: Can’t assign to the final variable ‘names’.
}
- `final` なリスト:リストという「箱」は変えられないが、中の「荷物(要素)」は追加・変更できる。
- `const` なリスト:箱も変えられないし、中の荷物も一歩も動かせない(完全に凍結された定数データ)。
この違いを頭に叩き込んでおくと、「あれ、なんでこのコードでエラーになるんだっけ?」と迷う時間が驚くほど減りますよ。
—
5. 現場で使える設計指針:どっちを使うべき?
実際の開発現場では、どのように使い分ければよいのでしょうか? 私がチームメンバーによく伝えている黄金律をシェアしますね。
1. 基本は `const` をファーストチョイスにする
- Widgetのコンストラクタや、アプリ内で使い回す固定値(カラーコード、設定値、パディングの数値など)は、可能な限り `const` にしましょう。Flutterのレンダリングパフォーマンスが跳ね上がります。
2. 実行時にしか決まらないもの、あるいはインスタンスの同一性を共有したくないものは `final` にする
- ユーザーからの入力値、APIから取得したJSONデータ、現在時刻など、実行時のコンテキストに依存するものはすべて `final` です。
—
まとめ
いかがだったでしょうか?
- `final` は、一度代入したら変えられない「実行時定数(鍵付きロッカー)」。
- `const` は、コンパイル時に値が完全に決まり、メモリ効率を極限まで高める「コンパイル時定数(石に刻まれた文字・共有財産)」。
この2つの違いを正確に理解していると、書くコードの質が一段も二段もレベルアップします。Dartが裏側でどう動いているのかを想像しながらコードを書けるようになると、プログラミングが何倍も面白くなりますよ。
ここをクリアできれば、Dartの基本はもうバッチリマスターです!自信を持って次のステップに進んでくださいね。それでは、また次回の記事でお会いしましょう!