【入門編】Dartの型プロモーションが効かないケース:ローカル変数とクラスフィールドの決定的な違い – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!Dartの世界へようこそ。
Flutterや最新のDart開発において、コードを書いていて「あれ?さっき `null` チェックしたはずなのに、なんでここでエラーが出るんだろう?」と首を傾げた経験はありませんか?

今回は、Dartの強力な武器である「型プロモーション(Type Promotion)」が、なぜか「クラスのフィールド」に対してだけは効かないという、多くの開発者が一度はハマる深い罠について解説します。

ここをクリアすれば、DartのNull安全やコンパイラの挙動に対する理解が一段と深まり、コードの美しさと堅牢性が劇的に変わりますよ。一緒にマスターしていきましょう!

—

1. そもそも「型プロモーション」って何でしたっけ?

Dartの型プロモーションは、コンパイラ(Dart VM / AOTコンパイラ)がコードの文脈を読み取り、「この変数は絶対に `null` じゃない」「この変数は特定のサブタイプに決まっている」とコンパイル時に賢く型を格上げしてくれる機能です。

まずは、ローカル変数での華麗な動きを見てみましょう。

void processString(String? text) {
// ここでは text は String? (nullかもしれない) 型

if (text == null) return; // nullなら早期リターン

// 【型プロモーション発動!】
// コンパイラ「おっ、ここで null チェックして弾いたから、
// この下で使われている text は絶対に String だな!」と判断する
print(text.toUpperCase()); // エラーにならない!
}

このように、ローカル変数の世界ではコンパイラが親切に型をプロモーションしてくれます。
「じゃあ、これをクラスのプロパティ(フィールド)でも同じようにやればいいよね?」と思ってしまいますよね。

—

2. なぜ「クラスのフィールド」だと型プロモーションが効かないのか?

ここに、TypeScriptなど他の言語からDartに来た開発者が驚くポイントがあります。
まずは次のコードを見てください。

class Box {
String? message;

void printMessage() {
if (message == null) return;

// 🔴 ここでコンパイルエラー!
// Error: The method ‘toUpperCase’ can’t be unconditionally invoked because the receiver can be ‘null’.
print(message.toUpperCase());
}
}

「えっ!?さっき `if (message == null) return;` で弾いたのに、なんでエラーになるの?」と思いますよね。
実はこれ、Dartのコンパイラがサボっているわけでも、バグでもありません。「マルチスレッド(Isolate)とオブジェクト指向の残酷な現実」に対する、Dartの極めて正確で安全な設計思想なのです。

技術的背景:エイリア싱(Aliasing)と非同期・別スレッドの恐怖

Dartのクラスフィールドは、どこからでも参照・変更される可能性があります。特にFlutterアプリ開発では、非同期処理(`async/await`)や、別のIsolate(Dartの並行処理単位)との絡みがあります。

コンパイラの視点になって考えてみましょう。

1. `if (message == null) return;` というチェックを通過した。
2. その直後、`print(message.toUpperCase());` が実行されるほんの一瞬の隙間(例えば、その間に別の非同期処理が割り込んだり、ゲッターがオーバーライドされていたりする場合)に、外部のコードや別のスレッドが `message = null;` に書き換えてしまったらどうなるでしょうか?

もし型プロモーションを許可してしまうと、チェックしたはずの瞬間に値が消滅し、実行時エラー(NullPointerExceptionの親戚)が起きてしまいます。
ローカル変数は「その関数(スコープ)の外から勝手に書き換えられるリスクが低い(※クロージャー等の例外を除く)」ため安全にプロモーションできますが、クラスのフィールドは「いつでも誰かに書き換えられる魔界」にあるため、コンパイラは信用してくれないのです。

—

3. 実務で使える!スマートな回避策

「じゃあ、クラスのフィールドを扱うときは、毎回毎回 `message!` って非nullアサーションを書かなきゃいけないの?それってダサいし危なくない?」

はい、その通りです。`message!` と書くのは、コンパイラへの「俺を信じろ!」という危険な賭けであり、なるべく避けたいアンチパターンです。

そこで、実務で使われる極上の回避策をご紹介します。それは「ローカル変数へのコピー」です。

黄金のテクニック:ローカル変数に退避させる

class Box {
String? message;

void printMessage() {
// 1. 一度、安全なローカル変数に代入する
final currentMessage = message;

// 2. ローカル変数に対して null チェックを行う
if (currentMessage == null) return;

// 3. ローカル変数なら型プロモーションが完璧に効く!
// currentMessage は確実に String 型として扱われる
print(currentMessage.toUpperCase()); // 完璧に動く!
}
}

なぜこれで解決するのか?

`final currentMessage = message;` とした瞬間、その時点での参照(あるいはプリミティブな値)がローカル変数にスナップショットとして固定されます。
ローカル変数は「魔界(クラスフィールド)」から切り離された安全地帯にあるため、Dartコンパイラは安心して型を `String` にプロモーションしてくれます。

—

4. ゲッター(Getter)を使っている場合も要注意

ちなみに、クラスのフィールドだけでなく、カスタムゲッターも同様の理由で型プロモーションは効きません。

class User {
String? get nickname => _nickname;
String? _nickname;

void greet() {
// ゲッターは呼ぶたびに違う値を返すかもしれない(外部書き換えやランダムなロジックなど)ため、
// if文のチェックは意味をなさず、型プロモーションは効かない!
if (nickname != null) {
// 🔴 エラーになる
// print(nickname.toUpperCase());
}

// 回避策:やはりローカル変数に受ける
final nick = nickname;
if (nick != null) {
print(nick.toUpperCase()); // OK!
}
}
}

—

まとめ

今回は、Dartの型プロモーションの裏側にある「安全性へのこだわり」を解説しました。

  • ローカル変数:安全なのでコンパイラが型プロモーションしてくれる。
  • クラスフィールド・ゲッター:外部から書き換えられるリスクがあるため、型プロモーションは効かない。
  • 解決策:一度 `final local = field;` とローカル変数に受けてから評価する。

Dartのコンパイラは、私たちのコードの安全性を守るために常に厳しく、そして優しく見守ってくれています。この挙動の理由を腑に落としておけば、もうNull安全のエラーに戸惑うことはありませんね。

ここをクリアしたあなたなら、Dartのメモリモデルやコンパイルの仕組みの核心にもグッと近づいています。
明日からのFlutter・Dart開発を、より自信を持って楽しんでいきましょう!

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