みなさん、こんにちは! DartとFlutterの世界へようこそ。
Dartは、Googleが妥協なく設計した非常に堅牢な「Sound Null Safety(健全なNull安全)」を備えた言語です。しかし、開発を進めていくと必ずぶつかる「壁」があります。それが非同期処理(Future / Stream)と Null安全の組み合わせです。
「`Future
「ライブラリのコードで見かける `FutureOr
こんな疑問を持ったことはありませんか?
大丈夫ですよ。この違いと本質さえ掴んでしまえば、Dartの非同期処理と型システムはバッチリマスターできます!
今回は、DartのコンパイラやDart VMが裏側で型をどう扱っているかという「本質」にも触れながら、現場で絶対に迷わなくなる知識を分かりやすく解説していきますね。
—
1. 静的型システムにおける Future と Null の位置づけ
まず前提として、Dartの型システムは「静的型解析(Static Analysis)」によってコンパイル時に強力に守られています。Dartにおける型システムは、一番上の頂点に `Object?`(すべての親)、一番底に `Never`(絶対に値を返さない型)が配置される美しい木構造をしています。
非同期処理の主役である `Future
では、ここに Null 許容演算子 `?` が付くとどうなるでしょうか?
実は、`?` を付ける位置によって全く異なる意味になります。ここが最初の重要ポイントです!
`Future` と `Future?` の図解的視点
【 Pattern A: Future
┌─────────────────────────────────┐
│ Future オブジェクトは「必ず存在する」 │
│ └─ 届いた箱を開けると: T か null │
└─────────────────────────────────┘
【 Pattern B: Future
┌─────────────────────────────────┐
│ Future オブジェクト自体が null かもしれない│
│ └─ そもそも開ける箱が存在しない可能性がある │
└─────────────────────────────────┘
- `Future
` - 意味: Futureという非同期の容器(Futureインスタンス)自体は確実に存在します。ただし、非同期処理が完了したとき、中に詰まっている結果が `T` 型の値か `null` のどちらかになります。
- 実務での使用率: 99% はこちらを使います。
- `Future
?` - 意味: Futureという容器そのものが `null` である(=非同期処理の呼び出しすら行われていない・存在しない)可能性があります。
- 実務での使用率: めったに使いません。非同期処理のキャッシュ初期化待ちフラグなどで特殊に使われる程度です。
—
2. `Future` の実践的な使い分けと型プロモーション
「非同期処理の結果として null が返ってくる可能性がある」という状況は、実務で頻繁に発生しますよね。例えば、データベースからの検索結果や、ネットワーク経由のデータ取得です。
コード例を見てみましょう。
/// ユーザーIDに基づいてデータベースからユーザー名を検索する関数
/// ユーザーが見つからない場合は null を返すため、戻り値は Future
Future
// 疑似的なネットワークレイテンシ(1秒待機)
await Future.delayed(const Duration(seconds: 1));
if (userId == 42) {
return ‘Dart Master’; // ユーザーが存在する場合
} else {
return null; // ユーザーが存在しない場合
}
}
Future
print(‘データ取得を開始します…’);
// 1. await することで、Future
final String? userName = await fetchUserName(100);
// コンパイルエラーの例:
// String name = await fetchUserName(100);
// ↑ String? は String に代入できないため、Dartコンパイラが安全にブロックします!
// 2. Null チェックによる「型プロモーション(Type Promotion)」
if (userName != null) {
// この scope(ブロック内)では、Dart VMは userName を「String」として確定評価します
print(‘ユーザー名: ${userName.toUpperCase()}’);
} else {
print(‘ユーザーが見つかりませんでした。’);
}
}
コンパイラとDart VMの視点
`await` キーワードを発行すると、Dart VMは現在のIsolate(実行スレッド)の処理を一時中断し、非同期処理が完了するとイベントループ経由で結果を回収します。
回収された値の型は `String?` です。それを `if (userName != null)` でチェックすると、Dartの高度な静的解析エンジンが「このブロック内では `userName` は絶対に `null` ではない」と判断します。これが型プロモーションです。
おかげで `userName.toUpperCase()` を安全に(`!.` などを使わずに)呼び出すことができるわけですね!
—
3. 謎の型 `FutureOr` の正体とパフォーマンス上の真実
さて、少しステップアップして、ライブラリや高度な設計でよく目にする `FutureOr
`FutureOr` とは何か?
直感的に言うと、`FutureOr
Dartの型システム上、以下のように定義されています。
`FutureOr
「えっ、全部 `Future
なぜ `FutureOr` が必要なのか?
Dart VMにおいて、`Future` を生成し `await` 処理を行うことには、極小ですがイベントループ(マイクロタスクキュー)を経由するオーバーヘッドが存在します。
もし「インメモリキャッシュにデータがあれば同期的に即座(0秒)で返したい」「キャッシュが無ければ非同期(Future)で取得したい」という場面を考えてみてください。
import ‘dart:async’; // FutureOr を使うためには dart:async のインポートが必要です
class UserCacheRepository {
final Map
/// キャッシュにあれば「String(同期)」、無ければ「Future
/// どちらを返しても戻り値の型 FutureOr
FutureOr
if (_memoryCache.containsKey(userId)) {
// 1. 同期処理:Futureで包まず、生の String を直接返す!
// マイクロタスクキューを経由しないため、超高速に評価されます。
return _memoryCache[userId]!;
}
// 2. 非同期処理:ネットワークから取得する Future
return _fetchFromNetwork(userId);
}
Future
await Future.delayed(const Duration(seconds: 1));
return ‘Network User ($userId)’;
}
}
Future
final repository = UserCacheRepository();
// 呼び出し側は、相手が同期(String)か非同期(Future)かを意識せず
// 一律 `await` で受け取ることができます!
// ID: 42 はキャッシュにあるので即座に値が得られる
final String user1 = await repository.getUserName(42);
print(‘User 1: $user1’);
// ID: 99 はキャッシュにないので非同期処理の完了を待って値が得られる
final String user2 = await repository.getUserName(99);
print(‘User 2: $user2’);
}
`FutureOr` のマトリクス整理
ここまでの話を整理すると、組み合わせは以下のようになります。
| 型定義 | コンテナ(Future)の状態 | 最終的に得られる値 | 主な用途 |
| :— | :— | :— | :— |
| `Future
| `Future
| `FutureOr
| `FutureOr
—
4. Streamにおける Null Safety の注意点
非同期処理といえば、連続するデータストリームを扱う `Stream
`Stream` においても Null Safety の考え方は全く同じです。
- `Stream
` : 流れてくるイベント(データ)は絶対に `null` にならない。 - `Stream
` : 途中で `null` という値自体がデータとして流れてくる可能性がある。
async と yield のコード例
/// カウントダウンを行うStream。途中で一時的に無効な値(null)を流す例
Stream
for (int i = count; i >= 0; i–) {
await Future.delayed(const Duration(milliseconds: 500));
if (i == 2) {
yield null; // 2の時だけ null を流す(Stream
} else {
yield i;
}
}
}
Future
// await for を使ってStreamから1つずつデータを取り出す
await for (final int? number in countdownStream(3)) {
if (number != null) {
print(‘Count: $number’);
} else {
print(‘Count: [データなし(null)]’);
}
}
}
`Stream
—
5. 初学者が陥りやすい「非同期 × Null安全」の2大罠と回避策
最後に、開発現場でよく見かける「コンパイルエラー」や「意図しない挙動」の原因となる罠について解説します。
罠 1: `await` の付け忘れによる Null チェックの勘違い
初心者の方が最もやりがちなミスです!
Future
void wrongExample() {
final result = fetchNullableData(); // ★ await を付け忘れている!
// result の型は String? ではなく「Future
if (result == null) {
// この条件は絶対に false になります!
// なぜなら result は「Futureというオブジェクトインスタンスそのもの」であり、
// Futureインスタンス自体は null ではないからです。
print(‘データはnullです’);
}
}
Future
// 正解: await を付けて、Future の中身(String?)を取り出してから比較する
final String? result = await fetchNullableData();
if (result == null) {
print(‘データはnullです’); // 正しく評価されます!
}
}
罠 2: `FutureOr` を普通の変数として宣言してしまう
`FutureOr
// 避けたほうがよい例
void doSomething(FutureOr
// input が String なのか Future
// await を付けずに String のメソッドを呼ぶとコンパイルエラーになります。
// print(input.length); // Error!
// 正解: 関数の内部では素直に await して String に統一して扱う
}
Future
final String value = await input; // await は通常の String に対しても安全に使用可能(そのまま返る)
print(value.length); // OK!
}
実は `await` は、対象が `Future` ではなく通常のオブジェクト(同期的な値)であってもエラーにならず、そのまま値を返してくれるという優しい性質を持っています。
—
まとめ:非同期とNull安全を制する者がDartを制す
今回は、Dartの非同期処理における型定義について深く潜り込んで解説しました。
1. `Future
2. `FutureOr
3. `await` をしっかり通すことで、Dartの静的解析エンジンが強力な「型プロモーション」を効かせて安全なコードにしてくれる。
ここさえクリアできれば、Dartの型システムやFlutterでの状態管理(BlocやRiverpodなど)のコードを読む際も、型の意味が手にとるように理解できるようになりますよ!
一歩一歩、本質を理解しながら楽しんでコードを書いていきましょう。応援しています!