こんにちは!FlutterやDartの開発現場で、日々コードを書いていらっしゃいますか?
今回は、Dartのコア文法の中でも、特にパフォーマンスとメモリ効率に直結する「`const`によるインスタンスの再利用とメモリ最適化」というテーマについて深く掘り下げていきます。
他のオブジェクト指向言語(JavaやC#など)からDartに入ってきた方ほど、「`final`と何が違うの?」「なぜそんなに`const`を推すの?」と疑問に思うかもしれません。ここをクリアすれば、Dartのメモリモデルの本質が見えてきて、ワンランク上のコードが書けるようになりますよ。
さあ、一緒にDartの奥深い世界へ足を踏み入れてみましょう!
—
1. なぜDartでは`const`がこれほど重視されるのか?
Dartにおいて、`const`は単なる「定数(値を変えられない)」という意味にとどまりません。最大の肝は、「コンパイル時に値を確定させ、メモリ上にただ一つのインスタンスを共有(キャッシュ)する」という点にあります。
まずは、日常の開発でよく見かける`final`と`const`の違いを、メモリの視点から整理しておきましょう。
- `final`: 「実行時」に一度だけ値を代入できる変数。中身のオブジェクトは毎回新しく作られる可能性があります。
- `const`: 「コンパイル時(ビルド時)」に完全に値が確定するもの。同じ定数式であれば、メモリ上に一度だけ生成され、以降はそれを使い回します。
イメージ図:インスタンスの生成と共有
【通常(finalやnew)の場合】
new Point(1, 2) → [ メモリ領域 A ] 生成!
new Point(1, 2) → [ メモリ領域 B ] 生成! (別モノとして扱われる)
【constの場合】
const Point(1, 2) → [ メモリ領域 S ] 生成!
const Point(1, 2) → [ メモリ領域 S ] を参照!(完全に同じインスタンス)
Flutterのアプリ開発で、何百個ものウィジェットを描画する際、もし毎回同じ設定のオブジェクトが新規生成されていたらどうなるでしょうか? ガベージコレクション(GC)が頻繁に走る原因になり、カクつき(フレーム落ち)を招いてしまいます。`const`を使うことで、この無駄なメモリ割り当てをゼロにできるのです。
—
2. 実践!`const`コンストラクタによるインスタンスのキャッシュ
では、実際にコードを書いて、`const`がどのようにメモリ上で振る舞うのかを確認してみましょう。
// イミュータブル(不変)なクラスの定義
class AppConfig {
final String appName;
final int version;
// constコンストラクタの定義
// クラスの全てのフィールドが final であり、コンストラクタ自体に const を付与します
const AppConfig({required this.appName, required this.version});
}
void main() {
// 1つ目のインスタンスを const で生成
const config1 = AppConfig(appName: ‘DartMaster’, version: 1);
// 2つ目のインスタンスを、全く同じ値で const 生成
const config2 = AppConfig(appName: ‘DartMaster’, version: 1);
// 3つ目はあえて const を使わずに生成(比較用)
final config3 = AppConfig(appName: ‘DartMaster’, version: 1);
// 同一性(メモリ上のアドレスが同じか)を検証する
print(‘config1 と config2 の同一性: ${identical(config1, config2)}’); // 予想は…?
print(‘config1 と config3 の同一性: ${identical(config1, config3)}’); // 予想は…?
}
実行結果と解説
このコードを実行すると、コンソールには次のように出力されます。
config1 と config2 の同一性: true
config1 と config3 の同一性: false
おやっ、と思いましたか?
`config1` と `config2` は、別々の行でインスタンス化しているにもかかわらず、`identical()`(Dart VM上で完全に同じメモリを指しているかを調べる関数)の判定が `true` になります。
Dartのコンパイラは、コンパイル時に「お、全く同じ `const` オブジェクトを作ろうとしているな」と気づき、2つ目以降の生成をスキップして、最初に作ったインスタンスの参照をそのまま割り当てる(=キャッシュする)のです。
一方で、`config3` は `final` で宣言されているため、実行時に新しくヒープ領域にメモリが確保され、`config1` とは別の存在になります。
—
3. ここでハマる!陥りがちな文法エラーと注意点
`const`を使いこなす上で、初学者がよくつまずくポイントがいくつかあります。コンパイラが怒ってくる理由をあらかじめ知っておけば、もう怖くありません。
エラーパターン1: フィールドが `final` になっていない
`const`コンストラクタを持つクラスのプロパティは、すべて `final`(または `const`)でなければなりません。
class BadConfig {
// ❌ コンパイルエラー!
// constコンストラクタのクラス内では、フィールドはすべて変更不能(final)でなければいけません。
String appName;
const BadConfig(this.appName);
}
理由: インスタンス化された後に中身が書き換わってしまったら、キャッシュした意味が崩壊してしまうからです。不変性(Immutability)の担保が `const` の大前提です。
エラーパターン2: 実行時の値を生で混ぜてしまう
`const`は「コンパイル時に決まる値」でなければならないため、実行時にしか分からない値を含めることはできません。
void main() {
// 現在時刻は「実行時」に決まるものですよね
final now = DateTime.now();
// ❌ コンパイルエラー!
// const オブジェクトの中に、実行時の値(now)は含められません。
const invalidConfig = AppConfig(appName: ‘Time’, version: 1);
// もし時間を扱いたいなら const は使えません
// final runtimeConfig = AppConfig(appName: ‘Time’, version: now.millisecondsSinceEpoch);
}
—
4. 先輩エンジニアからのアドバイス:Flutterでの活かし方
Flutterでウィジェットを書くとき、次のようなコードを見かけることはありませんか?
// 良くない例:再描画のたびに新しいインスタンスが作られる
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0), // ここ、実は毎回インスタンスが生成されています
child: Text(‘こんにちは’),
);
}
`EdgeInsets.all(16.0)` のようなマージンやパディング、あるいは引数を取らない子ウィジェットなどに `const` を適切につける(あるいは親に `const Text(…)` を使う)ことで、Flutterのツリー構造の差分検出(Reconciliation)のコストを劇的に下げることができます。
「迷ったらまずは `const` をつけられないか考えてみる」
これが、パフォーマンスの高いDart / Flutterコードを書くための最大の近道です。
—
まとめ
- `const` はメモリの救世主: 同じ定数式であればインスタンスがキャッシュされ、メモリ消費とGCの負荷を抑えられる。
- イミュータブルが前提: `const`コンストラクタを使うクラスは、すべてのフィールドが `final` である必要がある。
- コンパイル時が勝負: 実行時の値(`DateTime.now()` など)を `const` の中に持ち込むことはできない。
ここをクリアできれば、あなたの書くDartコードはぐっと洗練され、メモリ効率の意識が高いプロフェッショナルなものになります。ぜひ、今日のコードから `const` を意識的に散りばめてみてくださいね。
それでは、次のレクチャーでお会いしましょう!バッチリマスターしていきましょう!