【入門編】Dartのconstコンストラクタにおける「正規化(Canonicalization)」の仕組みと、メモリ上の同一性判定の裏側 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartを使った開発を楽しんでいますか?

他の言語からDartにやってくると、変数宣言の `var`、`final`、そして `const` の違いで少し手が止まることってありますよね。「なんとなく定数なんだろうな」と思って使っている方も多いのではないでしょうか。

今回は、その中でも一歩踏み込んだ「`const` コンストラクタにおける正規化(Canonicalization)の仕組みと、メモリ上の同一性判定の裏側」についてお話しします。

ここをクリアすれば、Dartのメモリ管理やオブジェクトの振る舞いに対する解像度がグッと上がり、自信を持ってコードを書けるようになりますよ。さあ、一緒に深掘りしていきましょう!

—

1. 変数宣言の基本おさらい:`var`・`final`・`const` の違い

まずは、おさらいとして基本の3つの違いをサクッと整理しておきますね。

  • `var`: 再代入可能な変数です。型推論が効くので非常に便利ですね。
  • `final`: 「一度だけ値(オブジェクト)を代入できる」変数です。実行時に値が決まります(ランタイム定数)。
  • `const`: 「コンパイル時定数」です。コードをビルドするその瞬間に値が確定していなければなりません。

この中で、今回主役になるのは `const` です。実は `const` は単に「書き換えられない」というだけではなく、Dartのパフォーマンスを裏から支える強烈な仕組みを持っています。それが「正規化(Canonicalization)」です。

—

2. `const` の本質:正規化(Canonicalization)とは何か?

皆さんは、次のようなコードを書いたとき、メモリ上で何が起きているかイメージできますか?

class Point {
final int x;
final int y;

const Point(this.x, this.y);
}

void main() {
var p1 = const Point(1, 2);
var p2 = const Point(1, 2);

print(p1 == p2); // true なのは分かる
print(identical(p1, p2)); // さて、これは何と表示されるでしょう?
}

結論から言うと、`identical(p1, p2)` の結果は `true` になります。

「あれ、別々に `const Point(1, 2)` ってインスタンス化したんだから、メモリ上のアドレスは別じゃないの?」と思いましたか?
ここにDartの秘密があります。Dart VMは、`const` で作られたオブジェクトに対して「同じ構造・同じ値を持つなら、メモリ上に1つしか作らない」という最適化を行います。

このプロセスを正規化(Canonicalization)と呼びます。

メモリ上のイメージ図

正規化が行われる世界と、行われない世界のイメージは以下のようになります。

【`final` や通常のインスタンス(正規化されない)】
`p1` ──> [ Pointインスタンス (x=1, y=2) ] (メモリ上のアドレス: 0x7f9a…)
`p2` ──> [ Pointインスタンス (x=1, y=2) ] (メモリ上のアドレス: 0x7f9b…)
※ 別々のメモリ領域を消費します。

【`const` インスタンス(正規化される)】
`p1` ┐
├─> [ たった1つだけ作られた Pointインスタンス (x=1, y=2) ]
`p2` ┘ (Dart VMの定数プール内で一元管理)
※ まったく同じ実体を指し示します!

Dart VMは内部に「定数プール(Constant Pool)」を持っていて、アプリが起動する(あるいはコードがコンパイルされる)ときに、重複する `const` オブジェクトを一つにまとめ上げてしまうのです。これにより、メモリ消費量が劇的に抑えられ、GC(ガベージコレクション)の負担も軽くなります。

—

3. 実践!コードでその挙動を確かめてみよう

実際に手を動かして、`const` の恩恵や挙動を確認してみましょう。以下のコードをコピーして試してみてください。

class ColorSettings {
final int red;
final int green;
final int blue;

const ColorSettings(this.red, this.green, this.blue);
}

void main() {
// 1. まったく同じ値を持つ const オブジェクト
const colorA = ColorSettings(255, 0, 0);
const colorB = ColorSettings(255, 0, 0);

// 2. リテラルで書かれたもの
var colorC = const ColorSettings(255, 0, 0);

// 3. 実行時に決定する値を含む場合(これは const にできない)
// int dynamicVal = 100;
// const colorError = ColorSettings(dynamicVal, 0, 0); // コンパイルエラー!

print(‘— 同一性の判定 (identical) —‘);
// colorA と colorB は同じインスタンスを指しているか?
print(identical(colorA, colorB)); // 輸出結果: true

// colorA と colorC も同じインスタンスを指しているか?
print(identical(colorA, colorC)); // 輸出結果: true
}

`identical()` 関数は、オブジェクトがメモリ上のまったく同じアドレスを指しているかを判定する関数です。ここが `true` になるということは、いくらあちこちで `const ColorSettings(255, 0, 0)` と書いても、Dartのメモリ上には実体が1つしか存在しないということを証明しています。

—

4. 陥りやすい文法エラーと注意点

`const` と正規化を使いこなす上で、初心者の開発者がハマりやすいポイントがいくつかあります。代表的なものを挙げておきますね。

① フィールドが `final` でなければ `const` コンストラクタを作れない

`const` オブジェクトは「イミュータブル(変更不可能な状態)」であることが絶対条件です。そのため、クラスのプロパティはすべて `final` である必要があります。

class BadExample {
int x; // NG: final がついていない
// const BadExample(this.x); // コンパイルエラー!
}

② `const` コンストラクタのなかで可変なオブジェクトを扱えない

例えば、リストやマップなどを `const` の中で使いたい場合、通常の `[]` や `{}` ではなく、`const []` のように明示的に `const` をつける必要があります。

class Team {
final List members;
const Team(this.members);
}

void main() {
// NG例:メンバのリストが const ではないため、コンパイルエラーになる
// var team = Team([‘Alice’, ‘Bob’]);

// 正しい例:リスト自体も const にする
const team = Team([‘Alice’, ‘Bob’]);
}

—

まとめ

いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ります。

1. `const` はコンパイル時定数を生み出す仕組みである。
2. Dart VMは、同じ値を持つ `const` インスタンスを定数プール内で一つに統合(正規化 / Canonicalization)する。
3. そのため、`identical()` で比較しても `true` になり、メモリ効率が極めて高くなる。
4. `const` コンストラクタを作るには、クラスのフィールドがすべて `final` である必要がある。

この正規化の仕組みを知っていると、FlutterなどでUIのコンポーネントを `const` 付きで定義すること(`const SizedBox(height: 16);` など)が、単なる気休めではなく、リビルド時の不要なメモリ割り当てを防ぐ強力なパフォーマンス最適化であるということが腑に落ちるはずです。

ここをクリアできれば、Dartのオブジェクトモデルの理解はバッチリマスターできていますよ!
明日からのコーディングで、ぜひ意識して `const` を使いこなしてみてくださいね。それではまた!

タイトルとURLをコピーしました