DartのNull安全と`const`の深淵:コンパイル時の「確定」を掌握せよ
Dartにおける「Sound Null Safety」は、単なるNullチェックの自動化ではない。それは、コンパイル時にメモリレイアウトと値の確定を保証し、実行時のランタイムエラーを根絶するための静的解析の要塞だ。
多くのエンジニアが`const`とNull許容型(Nullable)を組み合わせようとして、「なぜここでコンパイルエラーになるのか?」と首を傾げる。今日は、その裏側にある「定数評価」と「Null安全性」の衝突を紐解き、堅牢なプロダクションコードを書くための作法を伝授する。
—
1. なぜNull許容型と`const`は相性が悪いのか
Dartの`const`は「コンパイル時に完全に評価可能であること」を要求する。しかし、Null許容型(例: `int?`)が混ざると、コンパイラは頭を抱えることになる。
なぜなら、`const`は「その値が実行時に変更されないこと」だけでなく、「実行時のメモリ上の位置(参照)が唯一無二であること」を保証する必要があるからだ。
もし、`const`の初期化にNull許容型が絡むと、コンパイラは「その値が `null` なのか、それとも特定のオブジェクトの参照なのか」を、コンパイル時点の静的な情報のみで完全に区別しなければならない。これが複雑な定数式では、静的解析の決定不能問題に直結するため、Dartは安全側(保守的)に倒して制約をかけている。
—
2. 現場で嵌まる「コンパイル時定数」の罠と回避策
よくあるのが、`const`コンストラクタ内でNull許容型のフィールドを扱おうとして発生するエラーだ。
非効率なコード例
// 悪い例: constコンストラクタで不完全な制約をかけている
class Config {
final int? timeout; // Nullable
const Config(this.timeout);
}
// コンパイルエラー:もしtimeoutがnullの場合、
// 実行時の定数プールにおける整合性が保証できないケースが発生する
const config = Config(null);
解決策:`const`の「値」として確定させる
Null許容型を`const`で扱いたい場合、「Nullの代わりにデフォルト値を明示的に注入する」か、「定数としての状態(State)を分離する」のが定石だ。
class Config {
final int timeout; // 非Nullに昇格させる
// デフォルト値を持つconstコンストラクタ
const Config({this.timeout = 3000});
// もし「値が存在しない」ことを表現したいなら、
// Nullableではなく、専用の「空定数」を定義する
static const Config empty = Config(timeout: 0);
}
—
3. 実務で活きる「Null安全 × const」の設計パターン
UIコンポーネントやAPI設定でよく使う、「設定値がない場合に備える」設計。これを`const`の恩恵を維持したまま実現するには、「定数化されたデフォルト値」を静的メンバとして保持するのが最も美しい。
推奨されるプロダクションコード
class ApiPolicy {
final Duration timeout;
final String? baseUrl; // Nullableでもconstは可能だが…
const ApiPolicy({
required this.timeout,
this.baseUrl,
});
// 静的定数でデフォルトを管理し、コンパイル時に評価させる
static const defaultPolicy = ApiPolicy(
timeout: Duration(seconds: 30),
baseUrl: ‘https://api.production.com’,
);
}
なぜこれが強いのか:
1. メモリ効率: `ApiPolicy.defaultPolicy` はアプリ全体で一度だけ評価され、ヒープ上の同じメモリ領域を指し続ける。
2. Null安全: `required` とデフォルト値の分離により、コンストラクタ呼び出し時の型推論が極めて明確になる。
3. 保守性: APIの定義変更があっても、`ApiPolicy`の定義を修正するだけで全コードに波及する。
—
4. 伝説のアーキテクトからの助言:思考の転換
Dart VMは、`const`オブジェクトに対して「Canonicalization(正準化)」という処理を行う。これは、同じ値を持つ定数はメモリ上で同一のインスタンスとして扱うという最適化だ。
あなたが `const` でNull許容型をこねくり回そうとすると、この正準化のプロセスを阻害することになる。`const` を使う時は、以下の指針を脳に刻んでほしい。
- 「Nullableなフィールドは、本当に `const` に必要か?」
- ほとんどの場合、それは `final`(定数ではないが変更不可)で十分だ。`const` は「コンパイル時に計算し、バイナリに焼き付ける値」にのみ限定せよ。
- 「Nullか否か」の分岐をコンストラクタの外に出せ
- `const`コンストラクタに複雑なロジックを持ち込むな。それはコンパイラの仕事を増やし、あなたのプロダクトを重くするだけだ。
最後に
Sound Null Safetyは、あなたのコードに「型という名の規律」を与えるものだ。`const`とNull安全を正しく使い分けることで、実行時のIsolate(アイソレート)間の通信もメモリ効率が高まり、Flutterアプリのフレームドロップも防げる。
「動くコード」ではなく、「コンパイラが最も効率的に最適化できるコード」を書こう。それこそが、シニアエンジニアとそうでない者の決定的な差だ。