Dartの型システムを飼い慣らせ:`Stream
コードレビューをしていると、未だに「とりあえず型に `?` をつけてコンパイラを黙らせる」という残念な実装に出くわすことがある。DartのSound Null Safetyは、単なる「Nullポインタ例外を防ぐためのガードレール」ではない。あれは、コンパイラが君の代わりにメモリレイアウトと制御フローを数学的に証明するための強力な武器だ。
今回は、非同期処理の要である `Stream` を題材に、型定義の「位置」が実行時の挙動と設計の健全性にどう直結するかを解き明かす。
—
1. 記号の位置は「意味」の境界線
まずは、この2つの型定義の決定的な違いを、メモリ上の「何がNullになり得るのか」という視点で整理しよう。
- `Stream
` : ストリーム自体は必ず存在するが、流れてくる「データそのもの」がNullを許容する。 - `Stream
?` : ストリーム自体がNullになる可能性がある。つまり、ストリームの購読(listen)すら失敗するリスクがある。
なぜ `Stream?` が「悪」なのか
`Stream
原則として、ストリームは「空であってもストリームである(`Stream.empty()`)」べきであり、nullであるべきではない。
—
2. 実務で遭遇する「データがNullになり得る」設計
API連携で「値が存在しない可能性がある」レスポンスを扱う場合、`Stream
非推奨:安易なアンラップ
// 危険な例: 購読時に毎回Nullチェックが発生し、ロジックが汚染される
stream.listen((data) {
if (data != null) {
print(data.length);
}
});
推奨:型を絞り込み、純粋な層で扱う
プロダクションコードでは、`whereType` を使い、Nullを除外したストリームを早期に生成するのが定石だ。
/// 非同期APIから流れてくるデータストリーム
final Stream
// 1. nullを除外した「堅牢な」ストリームを生成
final Stream
// 2. 購読側はNullを一切気にしなくて良い
safeStream.listen((String validData) {
// ここには決してNullは来ない。コンパイラが保証している。
print(‘Processed: $validData’);
});
—
3. コンポーネント設計への応用:Null許容型を「状態」に変える
フロントエンド(特にFlutter)では、`StreamBuilder` がこの型定義の恩恵をダイレクトに受ける。`Stream
class DataStreamManager
// ストリーム自体はNullにならないよう保証(Finalで初期化)
final StreamController
Stream
void push(T? value) => _controller.add(value);
}
この設計の利点は、`dataStream` を購読するUIコンポーネントが「Nullが流れてくるかもしれない」という恐怖から解放されることにある。「Nullの処理」はストリームの入り口で一度だけ行い、UI層には「確定した型」を渡す。 これが保守性の高いアーキテクチャの鉄則だ。
—
4. チーフアーキテクトからの提言
最後に、Dart VMの挙動を意識したパフォーマンスの話をしよう。
Dartはnullチェックを `if` 文で行うと、その後のブロック内では型が昇格(Type Promotion)する。しかし、`Stream` のような非同期処理において、`await for` や `listen` の内部で頻繁にNullチェックを行うのは、コンパイラの最適化(JIT/AOT)の観点からも、CPU分岐予測の観点からも無駄が多い。
- 型は「境界」で制御せよ: 外部からの入力(API、DB、ユーザー操作)は `T?` として受け取り、処理の核心に入る前に `whereType` や `null` チェックで `T` に昇格させる。
- Optionalを隠蔽せよ: UIやビジネスロジックの深い階層に `?` を持ち込んではならない。`?` は可能な限りコードの周辺部で消費し、消滅させるべきだ。
`Stream
型安全とは、単にコンパイルを通すことではない。君の書いたコードが、実行時に「あり得ない状態」を排除していると数学的に保証することだ。 その誇りを持って設計に向き合ってほしい。