【実務・中級編】DartのNull安全と「Future/Stream」の非同期処理における型定義 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dartの型システムを飼い慣らせ:`Stream` と `Stream?` の境界線

コードレビューをしていると、未だに「とりあえず型に `?` をつけてコンパイラを黙らせる」という残念な実装に出くわすことがある。DartのSound Null Safetyは、単なる「Nullポインタ例外を防ぐためのガードレール」ではない。あれは、コンパイラが君の代わりにメモリレイアウトと制御フローを数学的に証明するための強力な武器だ。

今回は、非同期処理の要である `Stream` を題材に、型定義の「位置」が実行時の挙動と設計の健全性にどう直結するかを解き明かす。

—

1. 記号の位置は「意味」の境界線

まずは、この2つの型定義の決定的な違いを、メモリ上の「何がNullになり得るのか」という視点で整理しよう。

  • `Stream`: ストリーム自体は必ず存在するが、流れてくる「データそのもの」がNullを許容する。
  • `Stream?`: ストリーム自体がNullになる可能性がある。つまり、ストリームの購読(listen)すら失敗するリスクがある。

なぜ `Stream?` が「悪」なのか

`Stream?` を多用するということは、君のAPI設計において「ストリームが準備できていない状態(null)」をビジネスロジックの一部として許容していることを意味する。これは、コンポーネントが「ストリームがあるか確認する」という不要なチェックを繰り返す原因となり、コードの可読性を著しく下げる。

原則として、ストリームは「空であってもストリームである(`Stream.empty()`)」べきであり、nullであるべきではない。

—

2. 実務で遭遇する「データがNullになり得る」設計

API連携で「値が存在しない可能性がある」レスポンスを扱う場合、`Stream` を使うことになる。ここで `map` や `whereType` を適当に使うと、あっという間に実行時エラーの温床になる。

非推奨:安易なアンラップ

// 危険な例: 購読時に毎回Nullチェックが発生し、ロジックが汚染される
stream.listen((data) {
if (data != null) {
print(data.length);
}
});

推奨:型を絞り込み、純粋な層で扱う

プロダクションコードでは、`whereType` を使い、Nullを除外したストリームを早期に生成するのが定石だ。

/// 非同期APIから流れてくるデータストリーム
final Stream rawStream = fetchRemoteData();

// 1. nullを除外した「堅牢な」ストリームを生成
final Stream safeStream = rawStream.whereType();

// 2. 購読側はNullを一切気にしなくて良い
safeStream.listen((String validData) {
// ここには決してNullは来ない。コンパイラが保証している。
print(‘Processed: $validData’);
});

—

3. コンポーネント設計への応用:Null許容型を「状態」に変える

フロントエンド(特にFlutter)では、`StreamBuilder` がこの型定義の恩恵をダイレクトに受ける。`Stream` を流す場合、UI側は `AsyncSnapshot` を受け取ることになる。ここで重要なのは、「Nullであること」に意味を持たせるか、それとも「データが存在しない」状態として分離するかだ。

class DataStreamManager {
// ストリーム自体はNullにならないよう保証(Finalで初期化)
final StreamController _controller = StreamController.broadcast();

Stream get dataStream => _controller.stream.whereType();

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` を見たら「データが存在しない可能性を適切に処理できているか?」を問い、`Stream?` を見たら「なぜこのストリームは初期化すらされていないのか?」と疑うこと。

型安全とは、単にコンパイルを通すことではない。君の書いたコードが、実行時に「あり得ない状態」を排除していると数学的に保証することだ。 その誇りを持って設計に向き合ってほしい。

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