【実務・中級編】Dartの「final」と「const」を使い分けるための「評価タイミング」フローチャート – Dart コア文法・オブジェクト指向・Null安全解析バイブル

開発現場のコードレビューで、`var`、`final`、`const`の使い分けが曖昧なコードを見かけるたびに、私はこう問いかける。「その変数のライフサイクルとメモリ上の存在証明を、君はコンパイル時と実行時のどちらに委ねているのか?」と。

特にFlutterによるフロントエンド開発や、複雑な非同期API連携を伴うWeb/クロスプラットフォーム開発において、この選択を誤ることは、不必要なGC(ガベージコレクション)の誘発や、ランタイムエラーの温床を作ることに直結する。

今回は、Dartの`final`と`const`を決定的に分ける「評価タイミング」の真実を解き明かし、実務で一切の迷いを断ち切るための設計指針を授けよう。

—

1. 評価タイミングのパラダイムシフト:コンパイル時 vs 実行時

まず、Dart VMとコンパイラ(CtoJ / AOT)の内部挙動の観点から、両者の決定的な違いを定義する。

  • `const`(コンパイル時定数):

コードがコンパイルされる「その瞬間」に値が確定し、バイナリ(アセンブリ)のデータセグメントにハードコードされる。アプリのライフサイクルを通じてメモリ上に唯一のインスタンス(Canonicalized Instance)しか存在せず、事実上のイミュータブルなグローバルリソースとして振る舞う。

  • `final`(実行時イミュータブル):

変数への「再代入の禁止」をコンパイラに誓約するものであり、値が確定するのはプログラムが実行された「その瞬間(Runtime)」である。インスタンスはヒープ上に生成され、スコープを抜ければ(参照が切れれば)GCの回収対象となる。

この違いを無視して「とりあえず変更しないから`const`にしておこう」あるいは「よく分からないから全部`final`だ」というコードを書くエンジニアは、Dartの最適化エンジン(Tree ShakingやConstants Canonicalization)の恩恵を自ら捨てていると言わざるを得ない。

—

2. 【脳内トレース用】`final` vs `const` 決定フローチャート

コードレビューで迷ったら、脳内で以下のフローチャートを走らせてほしい。

[ 変数を宣言する ]
│
├─Q1: その値は「コードを書いた瞬間(コンパイル時)」に完全によみきれるか?
│ │
│ ├─ NO ──> 【 final 】 確定(実行時の動的評価、APIレスポンス、DateTime.now()など)
│ │
│ └─ YES ──> 次の質問へ
│
└─Q2: そのオブジェクトは「ディープ(再帰的)に不変」かつ「UIツリーや他クラスで定数として共有すべき」か?
│
├─ NO ──> 【 final 】(または場合により var)
│
└─ YES ──> 【 const 】 確定(コンパイル時定数、ウィジェットのプレースホルダーなど)

特筆すべきは、`const`は「そのオブジェクト構造の末端に至るまで完全に定数で構築されていなければならない」という点だ(Deep Constness)。途中に実行時でなければ決まらない値(例: `DateTime.now()`)が混ざった瞬間、それは`const`ではなくなり、`final`へと格下げされる。

—

3. プロダクションコードで示す:堅牢な設計パターン

では、実際のフロントエンド(Flutterコンポーネント設計)および非同期API連携を想定したコードで、この理論をどう実践に落とし込むかを示す。

以下のコードは、APIから取得したユーザーデータをもとにUIを描画し、かつ無駄な再描画やメモリ消費を防ぐために最適化されたプロダクション品質のコードである。

import ‘dart:async’;
import ‘package:flutter/material.dart’;

// ==========================================
// 1. ドメイン層・データ構造の定義
// ==========================================

/// ユーザー情報を表すイミュータブルなデータクラス
class UserProfile {
final String id; // 実行時決定(APIから取得)
final String username; // 実行時決定
final DateTime lastLoginAt; // 実行時決定(const不可!)

const UserProfile({
required this.id,
required this.username,
required this.lastLoginAt,
});
}

// ==========================================
// 2. コンポーネント設計・UI層
// ==========================================

class UserProfileCard extends StatelessWidget {
// 【設計ポイント】
// ウィジェットの設定値自体がアプリ起動時に確定しているため const コンストラクタを強制する。
// これにより、親ウィジェットが再描画されても、このインスタンスが不変であれば
// FlutterフレームワークはDiffingコストをゼロにできる(Const Evaluationの極み)。
const UserProfileCard({
Key? key,
required this.user,
}) : super(key: key);

final UserProfile user;

// 【設計ポイント】
// ウィジェット内で使い回す静的なスタイル定義は const にし、
// メモリ上のアロケーションを完全に排除する。
static const TextStyle _kBaseTextStyle = TextStyle(
fontSize: 14.0,
color: Colors.black87,
);

@override
Widget build(BuildContext context) {
// 【設計ポイント】
// メソッド内でスコープが完結し、かつ再代入されないローカル変数。
// 値が実行時(BuildContextやuserのプロパティ)に依存するため final を選択。
final String displayText = ‘User: ${user.username}’;
final Color statusColor = user.lastLoginAt.difference(DateTime.now()).inDays.abs() < 1 ? Colors.green : Colors.grey; return Container( // この EdgeInsets もコンパイル時定数として評価され、メモリ効率が良い padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8.0), ), child: Row( children: [ CircleAvatar( backgroundColor: statusColor, // const ウィジェットの活用:アイコン自体は不変なので const child: const Icon(Icons.person, color: Colors.white), ), const SizedBox(width: 12.0), // const layout spacer Text( displayText, style: _kBaseTextStyle, ), ], ), ); } } // ========================================== // 3. 非同期API連携・サービス層 // ========================================== class UserService { // アプリケーション全体で使い回すタイムアウト設定などは const static const Duration _apiTimeout = Duration(seconds: 10); /// ユーザーデータを非同期で取得するシミュレーション Future fetchUserData(String userId) async {
// 実行時に決まるエンドポイントやリクエストヘッダー
final String endpoint = ‘https://api.example.com/users/$userId’;

// ログ出力用のプレフィックス(コンパイル時定数でも良いが final でも許容範囲)
final DateTime requestTime = DateTime.now();
print(‘[$requestTime] GETing from $endpoint’);

// 非同期通信のモック
await Future.delayed(const Duration(seconds: 1));

// APIレスポンスのパース(当然、中身は実行時データなので final)
final Map responseJson = {
‘id’: userId,
‘username:’: ‘DartArchitect’,
};

return UserProfile(
id: responseJson[‘id’] as String,
username: responseJson[‘username:’] as String,
lastLoginAt: DateTime.now(), // 実行時評価!ここに const は使えない
);
}
}

—

4. テクニカルリードからの警鐘:よくあるアンチパターン

コードレビューで以下の記述を見つけたら、即座に修正を要求してほしい。

アンチパターン A: 「とりあえず const」病

// ❌ 誤り:DateTime.now() は実行時評価であるため、コンパイル時に確定できない。
// これを書くとコンパイルエラーになるが、無理やり回避しようとしてロジックを歪める人がいる。
const DateTime currentTime = DateTime.now();

修正: 実行時評価なのだから素直に `final DateTime currentTime = DateTime.now();` とする。

アンチパターン B: 巨大なオブジェクトツリーでの `const` 漏れ

// ❌ 効率の悪さ:内部に可変な要素や動的値がないにもかかわらず const をつけていない
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0), // 毎回インスタンスがヒープに生成される(GCの負荷増大)
child: Text(‘Hello’),
);
}

修正: `const EdgeInsets.all(16.0)` のように `const` を付与し、Dart VMの定数プール(Constants Canonicalization)の恩恵を受け、同一インスタンスを再利用させることでメモリ割り当てをゼロにする。

—

結びにかえて

Dartにおける`final`と`const`の選択は、単なるコーディング規約の好みの問題ではない。それは「メモリと実行時コストに対するエンジニアの意志表示」である。

コンパイル時に解決できるものはすべて`const`に閉じ込め、ランタイムに委ねるべきものだけを`final`で守る。この境界線を正確に引けることこそが、プロダクション環境でスケールする堅牢なアプリケーションを作り上げるための絶対条件だ。

次のプルリクエストを出す前に、もう一度自分の書いた変数の宣言を見直してほしい。その変数は、本当に正しい時間に評価されているか?

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