【入門編】Null安全を極める:Dartの「型昇格(Type Promotion)」が効かないケースと回避策 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!Dartの世界へようこそ。

Flutterの人気に伴い、Dartを学び始める方が本当に増えていますよね。Dartは非常にスマートで、書いていて心地よい言語です。その快適さを支えている最大の功労者が、Dart 2.12から導入された「Sound Null Safety(健全なNull安全)」です。

Null安全のおかげで、私たちは「実行時にアプリがNullのせいでクラッシュする(いわゆるぬるぽ:NullPointerException)」という恐怖から解放されました。そして、このNull安全を影で支える超優秀な仕組みが「型昇格(Type Promotion)」です。

しかし、Dartを書き進めていると、必ず一度は次のような壁にぶつかります。

「えっ? さっき `if (value != null)` ってチェックしたのに、なんで次の行で『Nullかもしれないからダメ!』ってコンパイラに怒られるの?」

今回は、そんな疑問をスッキリ解消するために、Dartの型昇格が「効くケース」と「効かない境界条件(罠)」、そしてそれを華麗に回避するプロの設計パターンを、コンパイラの裏側の動きも交えて優しく丁寧に解説します。

ここをマスターすれば、DartのNull安全はあなたの強力な味方になりますよ。さあ、一緒に深掘りしていきましょう!

—

1. そもそも「型昇格(Type Promotion)」ってなに?

まずは基本からおさらいしましょう。

Dartのコンパイラ(静的解析エンジン)は非常に賢く、コードの流れを上から順に追跡しています。これを「フロー解析(Flow Analysis)」と呼びます。

例えば、次のコードを見てください。

void printLength(String? text) {
// text は「String」または「null」の可能性がある型(String?)

if (text != null) {
// この if 文の中に入ったということは、絶対に null ではないですよね?
// だから Dart は、このブロック内だけで text の型を「String?」から「String」に「昇格」させます!
print(text.length); // エラーにならない!
}
}

本来、`String?` 型の変数からは直接 `.length` を呼ぶことはできません(Nullの可能性があるため)。しかし、`if (text != null)` という条件式を通過したことで、コンパイラは「よし、このブロック内では `text` は絶対にNullじゃない。`String` 型として扱っていいよ!」と自動的に型を格上げしてくれます。

これが型昇格(Type Promotion)です。開発者がいちいちキャスト(`text as String`)を書かなくてもいいように、Dartが気を利かせてくれているわけですね。

—

2. なぜ効かない?型昇格が拒否される「3つの境界条件」

ところが、この親切な型昇格が、急に「へそを曲げて」機能しなくなるケースがあります。
代表的な3つの罠を見てみましょう。

罠①:クラスの「フィールド(プロパティ)」の場合

もっとも多くの開発者が遭遇する「なぜ?」がこれです。

class User {
String? name; // Nullを許容するフィールド
}

void welcomeUser(User user) {
if (user.name != null) {
// nullチェックしたはずなのに…
// コレはコンパイルエラーになります!
print(‘Hello, ${user.name.length} letters!’);
// エラー内容: Property ‘length’ cannot be accessed on ‘String?’ because it is potentially null.
}
}

「直前で `user.name != null` ってチェックしたのに、なぜエラーになるの?」と不思議に思いますよね。

実は、これには「Dartのクラス設計の仕様」と「マルチスレッド(または別処理)による書き換えの可能性」という深い理由があります。

理由:Getterは毎回「違う値」を返すかもしれない

Dartにおいて、クラスのプロパティ(`user.name`)へのアクセスは、内部的には「Getter(ゲッター)というメソッドの呼び出し」と同じです。
そのため、以下のような実装が裏で作られている可能性があります。

class User {
// 呼び出すたびに、ランダムで null か文字列を返す悪魔的なゲッター
String? get name => DateTime.now().second % 2 == 0 ? ‘Alice’ : null;
}

もし、このようなゲッターだった場合、`if (user.name != null)` の判定時点では `Alice`(Nullじゃない)だったとしても、その次の行の `user.name.length` を評価する瞬間には、時間が進んで `null` を返してしまうかもしれません。

Dartのコンパイラは、この「一瞬の隙に値が変わるリスク」を絶対に許しません。これが、クラスのフィールドが直接型昇格できない本質的な理由です。

—

罠②:クロージャ(Closure)をまたぐローカル変数の場合

ローカル変数(関数の中で定義した変数)は本来、型昇格しやすい性質を持っています。しかし、「クロージャ(匿名関数やラムダ式)」が絡むと、昇格がキャンセルされてしまいます。

void processWithDelay() {
String? message = ‘Hello’;

if (message != null) {
// ここでは型昇格して `String` になっている
print(message.length); // OK!

// しかし、後から実行されるかもしれないクロージャの中に入ると…
Future.delayed(Duration(seconds: 1), () {
// コレはコンパイルエラー!
print(message.length);
// エラー: message が null かもしれないと警告されます
});
}

// なぜなら、後から message に null が代入される可能性があるため
message = null;
}

コンパイラから見ると、「1秒後に実行されるクロージャの中身」が動く前に、メインの処理の方で `message = null` が実行される可能性があります。
このように、「いつ実行されるかわからない関数(クロージャ)の中で、再代入可能な変数を参照する場合」、安全のために型昇格は無効化されます。

—

罠③:`var` で宣言した変数が途中で再代入される場合

ローカル変数であっても、その変数に「型昇格を打ち消すような代入」が後から行われる可能性がある場合、フロー解析が混乱して昇格が解かれることがあります。

void checkAndAssign() {
String? data = getNullableData();

if (data != null) {
// ここでは String に昇格している
print(data.length); // OK

// しかし、その後ろで Null になる可能性のある値を再代入していると…
data = getNullableData();

// この再代入コードが「後ろにあるだけ」で、
// コンパイラは慎重になり、昇格の維持を諦めることがあります
}
}

—

3. プロはこう書く!Null安全を確実に突破する「3つの回避策」

型昇格が効かない理由が分かれば、対策はとてもシンプルです。
Dartの設計思想に沿った、美しく安全な回避策をマスターしましょう。

回避策A:【王道】ローカル変数にコピーする(シャドウイング)

クラスのフィールドが昇格しない問題に対する、最もクリーンで確実な解決策は「一度ローカル変数に代入すること」です。

ローカル変数(特に `final`)は、一度代入したら外部から勝手に書き換えられる心配がありません。そのため、コンパイラも安心して型昇格を適用してくれます。

class User {
String? name;
}

void welcomeUser(User user) {
// 1. ローカル変数(final)にコピーする
final name = user.name;

// 2. ローカル変数側で Null チェックを行う
if (name != null) {
// ここでは「name」は完全に String 型に昇格しています!
print(‘Hello, ${name.length} letters!’); // 完璧に安全!
}
}

同じ `name` という変数名でローカル変数を作ることを「シャドウイング(影武者化)」と呼びます。コードの意図が明確になり、非常に可読性が高いパターンです。

—

回避策B:【モダン】Dart 3の「パターンマッチング」を使う

Dart 3以降を使っているなら、パターンマッチング(Pattern Matching)を使うことで、ローカル変数への代入とNullチェックを同時に、かつスタイリッシュに行うことができます。

`if-case` 文を使ったスマートな書き方

void welcomeUserDart3(User user) {
// user.name が Null でない場合、その値を「name」として取り出す
if (user.name case final name?) {
// このブロック内では、name は String 型として使えます
print(‘Hello, ${name.length} letters!’);
}
}

`case final name?` の末尾にある `?` は、「Nullではない場合」というフィルターを意味しています。
一行でスマートに書けて、かつスコープ(有効範囲)もこの `if` 文の中に閉じ込められるため、非常に美しいアプローチです。

—

回避策C:【シンプル】Null対応演算子 `?.` と `??` を活用する

そもそも `if` 文による分岐すら使わず、1行で簡潔にデフォルト値を割り当ててしまう方法も有効です。

void printUserLength(User user) {
// user.name が null なら 0、そうじゃなければ文字数を取得
final length = user.name?.length ?? 0;
print(‘長さは: $length’);
}

  • `?.` (条件付きアクセス演算子):Nullならそこで評価を止めてNullを返す。
  • `??` (Null合体演算子):左辺がNullなら右辺のデフォルト値を返す。

これらを組み合わせることで、型昇格そのものを意識する必要すらなくなります。

—

まとめ:仕組みを知れば、Null安全は最高の相棒になる!

最後に、今回のポイントを整理しておきましょう。

1. 型昇格(Type Promotion)は、コンパイラが「絶対にNullではない」と確信したときに型を格上げしてくれる機能。
2. クラスのフィールド(プロパティ)は、呼び出すたびに値が変わる(Getterの)可能性があるため、直接は型昇格できない。
3. 対策は、「一度 `final` なローカル変数にコピーする」、または 「Dart 3の `if-case` パターンマッチングを使う」 のが鉄板!

コンパイラが警告を出してくれるのは、意地悪をしているのではなく、「将来、あなたのアプリがクラッシュするのを全力で防いでくれている」からです。

この仕組みを理解して味方に付ければ、Dartでの開発が何倍も楽しく、そして安全になりますよ。ここをクリアできれば、Dartの基本はバッチリマスターできたも同然です!

一歩ずつ、楽しみながらコードを書いていきましょうね。応援しています!

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