【入門編】Null安全における『型昇格の遮断』を可視化する:クロージャと非同期処理の罠 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは。Dartの深淵へようこそ。

Dartの「Sound Null Safety」は、単なるエラーチェックの仕組みではありません。Dartのコンパイラが「この変数は絶対にNullにならない」と数学的に証明するための、非常に強力で厳格な静的解析エンジンです。

今日は、多くの開発者が「なぜかコンパイラが許してくれない」と頭を抱える「型昇格(Type Promotion)の遮断」という現象について、Dart VMの裏側にあるロジックを紐解きながら解説します。

—

1. 型昇格とは「コンパイラの確信」である

まず、型昇格の基本をおさらいしましょう。Dartのコンパイラは、コードの流れを追跡して「あ、この変数はここでNullチェックを通ったから、次は確実に値が入っているな」と判断します。

void process(String? input) {
if (input != null) {
// ここで input は String? から String に昇格する
print(input.length);
}
}

これは分かりやすいですよね。コンパイラは`if`文のスコープ内を単一の実行パスとして見ているため、「安全」だと確信できるからです。

—

2. なぜクロージャや非同期処理で「昇格」が遮断されるのか

ここからが本題です。以下のコードを見てください。

void main() {
String? name = “Dart”;

if (name != null) {
// ここは昇格する
print(name.length);

// クロージャ(関数)を定義
void printName() {
// エラー!: A value of type ‘String?’ can’t be assigned to a variable of type ‘String’.
// print(name.length);
}
}
}

なぜ、外側では`name`が`String`として扱えるのに、クロージャの中に入ると`String?`に戻ってしまうのでしょうか?

理由は「書き換えの可能性」

Dartのコンパイラは、「その変数が、クロージャの実行中に別の場所から変更される可能性」を常に警戒します。

  • 同期的なif文: 「今この瞬間」Nullでないことが確定している。
  • クロージャ/非同期処理: 「未来のいつか」実行される。その間に外部から変数が書き換えられているかもしれない。

コンパイラにとって、クロージャは「実行されるまで何が起きるか分からないブラックボックス」です。そのため、安全を確保するために「昇格した情報を信頼せず、元の型(Null許容型)に戻す」という防衛策をとるのです。これを「型昇格の遮断」と呼びます。

—

3. 非同期処理における「罠」の正体

非同期処理(`Future`や`await`)も全く同じ理由です。`await`を挟むと、その前後で変数の状態が保持される保証はありません。

Future update() async {
String? title = “Hello”;

if (title != null) {
// 昇格中
await Future.delayed(Duration(seconds: 1));

// エラー!: 再び title は String? に戻っている
// print(title.length);
}
}

`await`した瞬間に処理がいったん中断され、その間に別のイベントループが走って`title`が`null`に書き換えられる可能性がある……とコンパイラは考えます。この「時間的隔たり」が、型昇格の壁となるのです。

—

4. この壁をどう乗り越えるか?(実践的ベストプラクティス)

「じゃあどうすればいいの?」という問いに対して、プロの現場では以下の3つの手法を使い分けます。

A. ローカル変数にコピーする(最も推奨)

クロージャに渡す前に、昇格した値を別の「Null非許容のローカル変数」に退避させます。

if (name != null) {
final safeName = name; // ここでコピーすれば、safeName は String として固定される

Future(() {
print(safeName.length); // 問題なし!
});
}

コンパイラは「`safeName`は`final`であり、後から変わることはない」と確信できるため、型昇格は維持されます。

B. `!`(非Nullアサーション)を使う(自己責任)

確実にNullにならないロジックだと断言できるなら、`!`を使います。ただし、これはコンパイラに「責任は私が持つ」と宣言する行為です。乱用は禁物です。

C. `if (name case String s)` を活用する

最新のDart(パターンマッチング)を使うと、変数の束縛がより明確になります。

if (name case String s) {
// s はこのスコープ内で確実に String
print(s.length);
}

—

まとめ:コンパイラと仲良くなろう

型昇格が遮断されるのは、Dartが意地悪をしているからではありません。「コードの実行順序が複雑になっても、Null例外でアプリをクラッシュさせない」という、Dartの設計思想そのものなのです。

  • クロージャや非同期処理は「未来の実行」である
  • 未来のことはコンパイラにも保証できないため、一度安全地帯(ローカル変数)に保護する

この「変数の寿命とスコープ」を意識できるようになると、Dartのコードは驚くほど堅牢で美しいものに変わります。ここをクリアすれば、あなたはもうDartの型システムを完全に掌握したと言っても過言ではありません。

さあ、自信を持って次のコードを書きに行きましょう!

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