こんにちは。Dartの深淵へようこそ。
Dartの「Sound Null Safety(健全なNull安全)」は、実行時のNull参照エラーをコンパイル時に根絶する、非常に強力な武器です。しかし、この武器には「型昇格(Type Promotion)」という非常に賢い仕組みが組み込まれており、これが時として開発者を悩ませる「罠」になることがあります。
今回は、特に非同期処理やクロージャで頻発する「なぜかNullチェックが効かない」という現象の正体と、それを突破するためのプロの定石を解説します。ここをクリアすれば、Dartのメモリ管理と型システムの息遣いが手に取るようにわかるようになりますよ。
—
1. なぜDartは「型昇格」を諦めるのか?
まずは、基本の型昇格を見てみましょう。
void main() {
String? name = ‘Dart’;
if (name != null) {
// コンパイラは「ここは絶対にnullじゃない」と証明できるため
// String? 型が String 型に自動的に昇格される
print(name.length);
}
}
これは直感的ですよね。しかし、同じことを「クロージャ」の中でやろうとすると、Dartは急に慎重になります。
罠:クロージャによるキャプチャの不確実性
void main() {
String? name = ‘Dart’;
if (name != null) {
// 匿名関数(クロージャ)を作成
void printName() {
// エラー: The property ‘length’ can’t be unconditionally accessed
// because the receiver can be ‘null’.
print(name.length);
}
printName();
}
}
「いやいや、さっき `if (name != null)` でチェックしたじゃないか!」と思いたくなりますよね。なぜコンパイラはこれを許さないのでしょうか。
理由は「副作用の可能性」です。
Dartのクロージャは、外部変数を「参照」して保持します。もし `printName` が呼ばれるまでの間に、別のスレッドや別の処理が `name` を `null` に書き換えてしまったらどうなるでしょう?
Dartのコンパイラは、「外部から変数が書き換えられる可能性があるスコープ」においては、型昇格という信頼を一旦リセットするという設計思想を持っています。これが、クロージャ内で型昇格が効かない根本的な理由です。
—
2. 非同期処理で遭遇する「Nullの亡霊」
これは非同期処理(`Future`など)で最もよく遭遇する壁です。
void processData(String? data) async {
if (data != null) {
// 1秒待機する間に、外の世界では何が起きるかわからない
await Future.delayed(Duration(seconds: 1));
// エラー: ‘data’ はローカル変数だが、非同期の壁を越えると
// 途中で書き換えられた可能性をコンパイラは排除できない
print(data.length);
}
}
コンパイラから見れば、`await` は「一度処理を中断して別の作業を受け入れる場所」です。その間に `data` が誰かに書き換えられる可能性がゼロではない。だからこそ、健全性を守るために「昇格した型」を信用しなくなるのです。
—
3. 解決策:ローカル変数への「退避(キャプチャ)」
この問題を解決する最もエレガントで、かつDartの作法にかなった方法は、「昇格した状態をローカル変数に固定(退避)する」ことです。
安全な書き方
void processData(String? data) async {
// 1. まずNullチェックを行い、昇格した値を「別の変数」に代入する
final safeData = data;
if (safeData != null) {
// 2. 非同期処理の間、この safeData は不変(final)であり、
// 書き換えられる心配がないことをコンパイラに保証する
await Future.delayed(Duration(seconds: 1));
// ここで safeData は String 型として安全に扱える
print(safeData.length);
}
}
なぜこれで解決するのか?
`final safeData = data;` とした時点で、その時点での `data` の値(参照)が `safeData` という新しい変数にコピーされます。`safeData` は `final` なので、後から値を変更することができません。
コンパイラは「この変数は絶対に書き換えられない」と確信できるため、型昇格を維持したまま非同期の壁を越えることを許してくれるのです。
—
本質的なまとめ
- 型昇格は「スコープ内での不変性」に依存する:コンパイラは「途中で誰かによって値が変えられないか?」を常に監視しています。
- 非同期処理とクロージャは「書き換えの温床」:これらを使うときは、コンパイラが型昇格を解除するのを前提にコードを書く必要があります。
- 「一時変数への退避」は防御プログラミングの基本:Null安全なコードを書く際、`final` な一時変数に安全な値を退避させるのは、Dartにおける定石中の定石です。
ここを理解できれば、Dartのコンパイラと「対話」できるようになります。コンパイラがエラーを出すのは、あなたのコードを批判するためではなく、あなたが書いたプログラムが「将来的にNullでクラッシュするリスク」を未然に防ごうとしているからなのです。
この感覚が掴めれば、もうDartの型システムはあなたの強力な味方です。ぜひ、日々のコーディングで試してみてくださいね!