こんにちは!FlutterやDartを使った開発を楽しんでいますか?
今回は、Dartという言語の裏側を支える最も美しく、そして強力な仕組みの一つである「`const`コンストラクタとメモリ上の同一性(Canonicalization)」について深掘りしていきましょう。
「FlutterでUIを構築していると、何気なく`const`を付けるように教わったけれど、あれは一体裏側で何をやっているんだろう?」
そんな疑問を持ったことはありませんか?
ここをクリアすれば、Dartのメモリ管理や最適化の哲学が手に取るように分かるようになりますよ。優しく、かつ本質的な部分まで丁寧に解説していくので、ぜひ最後までついてきてくださいね。
—
1. まずはおさらい:`var`、`final`、`const`の違いの本当の意味
Dartの変数宣言には `var`、`final`、そして `const` がありますよね。初学者のうちは「どれも値を固定するものなんでしょ?」と思いがちですが、コンパイラの視点で見ると、生きている世界が全く違います。
- `var`: 変数に最初に入れた型を推論し、後から別の値も代入できる(再代入可能)。
- `final`: 実行時(Runtime)に一度だけ値を代入でき、その後は変更できない。
- `const`: コンパイル時(Compile-time)、つまりコードが実行されるよりはるか前、ビルドの段階で完全に値が確定していなければならない。
特に `const` は、Dartにおいて「単なる定数」以上の意味を持ちます。それは「プログラムの全ライフサイクルを通じて、メモリ上にたった1つしか存在しないことをコンパイラが保証する印」なのです。
—
2. メモリの「同一性」とは何か?(`==` と `identical` の違い)
まずは、次のコードを見てください。普通にクラスのインスタンスを2つ作った場合、メモリ上ではどう扱われるでしょうか?
class Point {
final int x;
final int y;
const Point(this.x, this.y);
}
void main() {
// 通常のインスタンス生成(constなし)
var p1 = Point(1, 2);
var p2 = Point(1, 2);
print(p1 == p2); // ① trueになる? falseになる?
print(identical(p1, p2)); // ② trueになる? falseになる?
}
おや? `Point` クラスには `==` のオーバーライドを書いていません。この場合、①の `p1 == p2` は、デフォルトではメモリ上の参照先が違うため `false` になります(構造的な等価性を比較するには `==` をオーバーライドする必要がありますよね)。
そして、②の `identical(p1, p2)` はどうでしょう?
これは「メモリ上のアドレス(居場所)が完全に一致しているか」を調べるDartの関数ですが、これも当然 `false` です。なぜなら、`p1` と `p2` はそれぞれ別のメモリ領域(ヒープ領域)に新しく領域を確保して作られた、別々のオブジェクトだからです。
では、ここに `const` を魔法のスパイスとして投入してみましょう。
—
3. `const` コンストラクタと「正規化(Canonicalization)」の裏側
先ほどのコードのインスタンス生成時に `const` をつけてみます。
void main() {
// constをつける!
const p1 = Point(1, 2);
const p2 = Point(1, 2);
print(p1 == p2); // true
print(identical(p1, p2)); // なんと… true になる!
}
なんと、`identical(p1, p2)` が `true` になりました!
別々の行で、別々に `Point(1, 2)` と書いているにもかかわらず、Dartのランタイム(Dart VM)は「おや、中身が全く同じ `const` オブジェクトがすでに存在しているな。じゃあ新しくメモリを確保するんじゃなくて、既存のやつを使い回そう!」と判断します。
この「同じ内容の定数は、メモリ上のたった一つのインスタンスにまとめ上げる(一意化する)」プロセスのことを、コンピュータサイエンスの用語で Canonicalization(正規化 / カノニカライズ) と呼びます。
Dart VMの定数プール(Constant Pool)の動き
Dartのコンパイラ(AOT/JITコンパイラ)やDart VMは、コードを解析する際に「定数式(Constant Expressions)」を見つけると、それをアセンブリやスナップショット内の「定数プール(Constant Pool)」という特別な領域に詰め込みます。
1. コンパイル時に `Point(1, 2)` という定数構造体が評価される。
2. Dart VMは内部のハッシュテーブル(定数プール)を確認し、すでに `x=1, y=2` を持つ `Point` が登録されていないか探す。
3. なければ新しく登録し、あれば既存のメモリ上のアドレスを返す。
これにより、どれだけアプリ内で同じ `const Point(1, 2)` を呼び出しても、メモリ上に消費される実体は世界にたった一つだけになります。これが、Flutterで `const` を推奨する最大の理由の一つ(メモリ割り当てとGCの負荷激減)です。
—
4. 陥りやすい文法エラーと「constの伝播」のルール
この強力な `const` ですが、使いこなすにはDart特有のルールを知っておく必要があります。よくあるハマりポイントを見ていきましょう。
ハマりポイント①:中身もすべて const でなければならない
`const` コンストラクタを呼び出すとき、渡す引数もすべてコンパイル時に確定する値(定数)でなければなりません。
void main() {
int dynamicX = 1;
// ❌ コンパイルエラー!
// 「dynamicX は実行時にしか決まらない変数なので、constの引数には使えません」
const p = Point(dynamicX, 2);
}
【解決策】 実行時の変数を使う場合は、`const` ではなく `final` を使いましょう。
ハマりポイント②:フィールドもすべて `final` である必要がある
`const` コンストラクタを持つクラスを定義するとき、クラス内のすべてのインスタンス変数(フィールド)は必ず `final` で宣言されていなければなりません。
class BadPoint {
int x; // ❌ コンパイルエラー! constコンストラクタを持つクラスのフィールドはfinalでなければなりません
int y;
const BadPoint(this.x, this.y);
}
なぜなら、`const` で作られたオブジェクトは「不変(Immutable)」であることが絶対条件だからです。途中で値が変わってしまったら、メモリ上の同一性を保証した意味がなくなってしまいますよね。
—
5. まとめ:ここをクリアすれば、Dartの基本はバッチリ!
今回は、`const` コンストラクタがメモリ上の同一性を保証する仕組み(Canonicalization:正規化)について、Dart VMの裏側の動きを交えて解説しました。
- `const` はコンパイル時に値を確定させ、メモリ上に唯一のインスタンスを共有(Canonicalization)する。
- `identical()` を使うと、それらがメモリ上の全く同じアドレスを指している(`true`)ことが確認できる。
- クラスを `const` にするためには、すべてのフィールドが `final` であり、渡す引数もすべて定数である必要がある。
Flutterでウィジェット(例: `const SizedBox(height: 16)`)を書くとき、この正規化の恩恵を受けて、無駄なメモリ消費やガベージコレクション(GC)の走る頻度を劇的に抑えています。
この仕組みを理解していると、コードを書くときの「なぜここで `const` をつけるべきなのか」という解像度がグッと上がります。
ここをクリアできたあなたは、もうDartのメモリモデルの核心を掴んでいますよ!自信を持って次のステップへ進んでくださいね。