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

コードレビューの現場で、次のようなコードに出くくしたことはないでしょうか。

// レビュー対象コード
const _defaultTimeout = Duration(seconds: 30);
final _appConfig = AppConfig(theme: Theme.dark());

一見して「動くから問題ない」で見過ごされがちですが、DartのコンパイラとVMの挙動を深く理解している者からすれば、ここには「ランタイム評価とコンパイル時評価の境界に対する無知」という致命的な設計の綻びが潜んでいます。

フロントエンドのコンポーネント設計、状態管理、そして非同期API連携を支える基盤において、`var`、`final`、`const`の選択は、単なるスタイルの好みではありません。それは「メモリ割り当ての最適化」「イミュータビリティの保証」、そして「Isolate間でのデータ共有の可否」を決定づける極めて重要なアーキテクチャ上の意思決定です。

今回は、Dartの心臓部を知るチーフアーキテクトの視点から、`final`と`const`を完全に掌握し、バグの起きない堅牢なコードベースを構築するための極限の知見を伝授します。

—

1. 評価フェーズの断絶:ランタイム vs コンパイラ

Dartの変数を語る上で絶対に外せないのが、「いつその値が確定し、どこに配置されるか」という評価フェーズの概念です。

[Source Code]
│
├─> const ──> 【コンパイル時】 定数プール (Canonicalized Constants)
│
└─> final ──> 【ランタイム】 ヒープ/スタック (一度だけ代入可能)

`const`:コンパイル時定数(Compile-time Constants)

`const`がついた値は、アプリケーションが実行される前、すなわちコンパイルの時点で完全に評価されなければなりません。
Dartのコンパイラ(AOT/JIT)は、これらをバイナリ内の「定数プール(Constants Pool)」に埋め込みます。驚くべきことに、アプリ内でどれだけ大量に同じ構造の`const`オブジェクトを作ろうとも、Dart VMはメモリ上に単一のインスタンス(Canonicalized Instance)しか生成しません。ポインター比較(`==` ではなく `identical()`)がO(1)で真になるのはこのためです。

`final`:ランタイム単一代入(Runtime Final)

`final`は、コンパイル時には値が決まっていません。プログラムが実行され、その行に到達した瞬間に一度だけ値がバインドされる変数です。
一度代入されたら再代入は不可能ですが、指し示している先がミュータブル(可変)なオブジェクトであれば、その内部状態は変更可能です。メモリ上では通常のヒープ領域にアロケーションされます。

—

2. 迷えるエンジニアのための「評価判断フローチャート」

コードレビューで「ここは`const`にするべきか、`final`か?」と迷ったときは、脳内で以下のフローチャートをトレースしてください。

[変数を定義する]
│
├─ Q1. 値はビルド時(コンパイル時)に完全に確定しているか?
│ ├─ No ──> `final` を使用 (ランタイム評価)
│ └─ Yes ──> 次の質問へ
│
│
├─ Q2. その値(またはオブジェクトグラフ全体)は、一切の可変状態を持たないか?
│ ├─ No ──> `final` を使用 (例: 内部にListを持つイミュータブルに見えるオブジェクト)
│ └─ Yes ──> 次の質問へ
│
│
└─ Q3. コンストラクタや関数呼び出しの結果ではなく、リテラルまたは`const`コンストラクタのみで構成されているか?
├─ No ──> `final` を使用 (例: Duration(seconds: 30) はランタイム評価されるため実はconstではない!)
└─ Yes ──> 🌟 `const` を使用すべき!

> ⚠️ 致命的な誤解への警告
> 先ほどの冒頭のコードで挙げた `const _defaultTimeout = Duration(seconds: 30);` は、Dartのバージョンによってはコンパイルエラー、あるいは暗黙的なランタイム評価(`const`の伝播漏れ)を引き起こす典型的なアンチパターンです。`Duration`のコンストラクタには`const`が付与されていますが、引数や文脈によってはランタイムに評価され、定数プールに入らない無駄なオブジェクトを生み出します。

—

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

では、実際のフロントエンド(Flutter / Web)のAPI連携やコンポーネント設計を想定した、保守性の高いコードを見てみましょう。
ここでは、「不変性の強制」「メモリ効率の最大化」「Isolate境界を意識した設計」を完璧に満たす実装例を提示します。

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

/// 【設計の意図】
/// APIクライアントの設定値は、アプリ起動時に決定され二度と変化してはならない。
/// しかし、エンドポイントのURLなどは環境変数(ランタイム)に依存するため `final` を選択。
class ApiClientConfig {
final String baseUrl;
final Duration timeout;
final Map defaultHeaders;

const ApiClientConfig({
required this.baseUrl,
required this.timeout,
required this.defaultHeaders,
});
}

/// 【設計の意図】
/// UIのパディングやウィジェットの構造など、完全に静的なレイアウト定義は
/// すべて `const` 化し、FlutterのElementツリーの再描画コスト(Reconciliation)を極限までゼロにする。
class AppConstants {
// 完全にコンパイル時評価されるプリミティブ定数
static const String appName = ‘Enterprise Nexus’;
static const int maxRetryCount = 3;

// constコンストラクタを持つオブジェクトのディープな定数化
static const EdgeInsets defaultScreenPadding = EdgeInsets.all(16.0);

// ⚠ 注意: 以下の配列はコンパイル時定数。内部要素も含めて完全にイミュータブル。
static const List supportedLocales = [‘en’, ‘ja’, ‘es’];
}

/// 【実務で使える堅牢なAPIフェッチサービスクラス】
class ApiService {
// コンパイル時定数によるヘッダーのデフォルト値(メモリ共有される)
static const Map _baseHeaders = {
‘Content-Type’: ‘application/json’,
‘Accept’: ‘application/json’,
};

final ApiClientConfig _config;

// 依存性注入(DI)により設定を受け取る。インスタンス変数は final でイミュータブル性を担保。
ApiService({required ApiClientConfig config}) : _config = config;

Future> fetchUserData(String userId) async {
// ランタイム評価される非同期のタイムアウト値
// constではなくfinalを使うことで、動的なコンフィグ変更に追従できる
final currentTimeout = _config.timeout;

try {
// 模擬的なAPIリクエスト処理
final stopwatch = Stopwatch()..start();

// ログ出力などのメタデータも final で安全に保持
final requestUri = Uri.parse(‘${_config.baseUrl}/users/$userId’);

debugPrint(‘[API] GET: $requestUri (Timeout: ${currentTimeout.inSeconds}s)’);

// ネットワークI/Oのシミュレーション
await Future.delayed(const Duration(milliseconds: 500));

stopwatch.stop();

// 戻り値のマップはランタイム生成されるため final
final responseData = {
‘id’: userId,
‘name’: ‘Dart Architect’,
‘fetchedAt’: DateTime.now().toIso8601String(), // ランタイム評価(現在時刻)
‘latencyMs’: stopwatch.elapsedMilliseconds,
};

return responseData;

} on TimeoutException catch (e, stackTrace) {
// エラーハンドリングにおけるイミュータブルな例外伝播
throw ApiException(‘Network request timed out: $e’, stackTrace);
}
}
}

/// カスタム例外クラスもイミュータブル(final変数のみで構成)に設計する
class ApiException implements Exception {
final String message;
final StackTrace stackTrace;

const ApiException(this.message, this.stackTrace);

@override
String toString() => ‘ApiException: $message’;
}

—

4. チーフアーキテクトからのメッセージ:なぜこの設計が不可欠なのか

上記のコードを見て、「ここまで厳密に使い分ける必要があるのか?」と感じたとしたら、それは大規模開発におけるパフォーマンスのボトルネックを踏んだことがない証拠です。

1. Flutterのウィジェット再描画最適化
UIコンポーネントのツリー内で、親が再描画された際、子孫のウィジェットや引数に渡されるオブジェクトが`const`であるならば、Flutterフレームワークは差分比較(Diffing)を一切スキップします。これが数千ノード規模のリスト画面や複雑なアニメーションを持つWebアプリにおいて、60fps/120fpsを維持する唯一の生命線です。
2. Isolate間通信(SendPort / ReceivePort)の安全性
Flutter/DartのマルチスレッドモデルであるIsolate間でデータを渡す際、オブジェクトはシリアライズ(コピー)されます。しかし、`const`で完全に定数化されたオブジェクトや、ディープにイミュータブルなオブジェクトは、コピーコストを劇的に削減できるアーキテクチャ上の恩恵があります。
3. バグの予兆をコードレベルでコンパイラに検知させる
「変えたくない値」に`final`を、「絶対に動かさない定数」に`const`を使う。この規律をチーム全体で徹底することで、意図しない副作用(Side Effect)によるバグの温床をコンパイルエラーとして事前に根絶やしにできます。

変数宣言の数文字の選択が、あなたの書くアプリケーションのパフォーマンスと寿命を決めます。
明日からのコードレビューでは、`var`、`final`、`const`の境界線に妥協のない鋭いメスを入れてください。それこそが、真のDartマスタリーへの道です。

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