こんにちは!FlutterやDartの開発現場で、日夜コードと格闘されている皆さん、順調ですか?
今回は、Dart 3で導入された`sealed class`(シール堂クラス)と、強力無比な網羅性チェック(Exhaustiveness Checking)について、一歩踏み込んで解説していきますね。
「switch式を書いたときに、なぜかコンパイルエラーになって焦った……」
「将来サブクラスが増えたときにも耐えられる、安全なデフォルトの書き方ってどうするの?」
そんな疑問を持ったことはありませんか?
ここをクリアすると、あなたの書くDartコードの安全性と保守性は劇的に跳ね上がります。一緒に本質をマスターしていきましょう!
—
1. `sealed class` とは何か?(基本のおさらい)
まずは、`sealed class`がDartのコンパイラ(AOT/JITコンパイラ)や型システムの中でどう扱われているのか、その本質を押さえましょう。
通常の `class` は、プログラム内のどこからでも(場合によっては別パッケージからでも)自由に対象を継承(extends)したり、実装(implements)したりできますよね。しかし、これではコンパイラ側から見て「このクラスのサブクラスは、一体世界中にいくつ存在するのか」を静的に確定できません。
ここで登場するのが `sealed` 修飾子です。
sealed class Result
class Success
final T data;
Success(this.data);
}
class Failure
final Object error;
Failure(this.error);
}
コンパイラにとっての `sealed` の意味
`sealed class` を定義すると、Dartのコンパイラは「このクラスを直接継承・実装できるのは、このファイル(ライブラリ)の内部で定義されているサブクラスだけだ」という厳格な制約を認識します。
つまり、コンパイル時に「サブクラスの全リスト」が完全に特定できるわけです。この強力な前提があるからこそ、Dart 3の `switch` 式における「網羅性チェック」が可能になります。
—
2. 網羅性チェックの魔力と「コンパイルエラー」の正体
Dart 3の `switch` は、文(statement)ではなく式(expression)としても書けるようになり、パターンマッチングの能力が飛躍的に向上しました。
例えば、先ほどの `Result` を受け取ってメッセージを返す関数を書いてみましょう。
String handleResult(Result
return switch (result) {
Success(data: val) => ‘成功: $val’,
Failure(error: err) => ‘失敗: $err’,
};
}
お気づきですか? ここには `default` ケースや `_`(ワイルドカード)がありません。それでもこのコードは完璧にコンパイルを通過し、正常に動作します。
なぜなら、Dartのコンパイラが「`Result` のサブクラスは `Success` と `Failure` の2つだけだから、この `switch` 式はすべてのケースを網羅している(Exhaustive)」と証明できるからです。
もしここで、将来的に新しいサブクラス `Loading` を追加したとします。
sealed class Result
class Success
class Failure
class Loading
すると、先ほどの `handleResult` 関数は、一文字もコードを変更していないにもかかわらず、突然コンパイルエラーを吐き出します。
> “The type ‘Result
> (型 ‘Result
これが網羅性チェックの真骨頂です!
「おっと、新しいサブクラス `Loading` が追加されたのに、君の `switch` 式では処理が漏れているよ!」と、コンパイラが未然にバグを防いでくれるのです。素晴らしい仕組みですよね。
—
3. 実践:将来の拡張性と「未知のサブクラス」をどう扱うか?
さて、ここからが本題です。
開発を進めていると、こんなジレンマに直面することがあります。
- 「網羅性チェックの恩恵を受けたいから、基本は `sealed` で型安全に書きたい」
- 「でも、将来的にサードパーティ製や別モジュール、あるいは頻繁に追加されるステータスなど、全てのサブクラスをわざわざ網羅しきれない(または、未知のサブクラスが来たらとりあえず安全にフォールバックさせたい)ケースがある」
すべてのケースをベタ書き強制されると、メンテナンスが大変になりますよね。
では、「網羅性チェックを維持しつつ、安全にデフォルトケース(未知のサブクラスへの備え)を実装する設計パターン」を見ていきましょう。
解決策:ワイルドカード(`_`)を使った安全なフォールバック
Dart 3では、アンダースコア `_` がワイルドカードパターンとして機能します。「その他すべて」をキャッチする表現です。
しかし、単純に `_ => …` を書くだけでは、網羅性チェックが無効化(またはバイパス)されてしまい、新しいサブクラスを追加したときにコンパイラが教えてくれなくなってしまいます。
これでは本末転倒です。ではどうするか?
正解は、「既知のケースをすべて網羅した上で、残りの型を `Never`(または想定外)として扱う」、あるいは「将来拡張されることを前提としたマーカーを挟む」という設計アプローチを取ることです。
実用的なコード例を見てみましょう。
sealed class AppState {}
class Initial extends AppState {}
class Loading extends AppState {}
class Authenticated extends AppState {}
// 将来、ここに Unauthenticated や Maintenance などが追加されるかもしれない…
String getScreenName(AppState state) {
return switch (state) {
Initial() => ‘スプラッシュ画面’,
Loading() => ‘ローディング画面’,
Authenticated() => ‘ホーム画面’,
// ▼ ここがポイント!
// 既知のサブクラスをすべて網羅しつつ、
// 万が一「未知のサブクラス(または将来追加されて書き忘れたもの)」が流れ込んできた場合の安全地帯
_ => _handleUnknownState(state),
};
}
String _handleUnknownState(AppState state) {
// ログ出力や、フォールバック用のデフォルト画面を返すなどの安全措置
print(‘警告: 未知のAppStateが検出されました: ${state.runtimeType}’);
return ‘デフォルト画面’;
}
この設計の何が優れているのか?
1. コンパイラの網羅性チェックは生きている
もし、新しいクラス `class Maintenance extends AppState {}` を追加したとき、もし `switch` 側に `Maintenance()` を書き忘れると、コンパイラはちゃんと警告・エラーを出してくれます。
2. 安全なデフォルト動作の担保
開発者が意図的に(あるいはライブラリの拡張などで)想定外の型が流れてきた場合でも、アプリがクラッシュ(StateError)するのを防ぎ、`_handleUnknownState` でエレガントに受け止めることができます。
—
4. 陥りやすい文法エラーと注意点
ここで、初心者の開発者の方がよくハマりがちな罠をいくつかご紹介しておきますね。
罠1:`default:` キー워ードを書いてしまう
これまでの古いDartや他の言語(JavaやC#など)の感覚で、以下のように書きたくなるかもしれません。
// ❌ やりがちなミス
switch (state) {
Initial() => ‘A’,
Loading() => ‘B’,
default => ‘C’, // Dart 3のswitch式では ‘default’ は使えない、または非推奨・警告の対象になることが多い
}
Dart 3の `switch` 式では、キーワードとしての `default:` ではなく、パターンマッチングとしてのワイルドカード `_` を使うのがイディオムです。
罠2:不要な型チェックを書いてしまう
`sealed class` の中身を判定するとき、以下のように冗長なコードを書く必要はありません。
// ❌ 冗長なコード
if (state is Initial) {
return ‘A’;
} else if (state is Loading) {
return ‘B’;
}
Dartのフロート解析(Flow Analysis)とパターンマッチングは非常に優秀です。`switch (state)` を使えば、各アーム(分岐)に入った時点で自動的にそのサブクラスへとキャスト(型昇格)されるため、`state.data` のようなプロパティにも安全にアクセスできます。
—
まとめ:Dartを掌握する者へのステップ
いかがだったでしょうか? 今回のポイントをギュッとまとめます。
- `sealed class` は、コンパイラに「サブクラスの全リスト」を知らせるための強力な契約。
- Dart 3の `switch` 式と網羅性チェック を使えば、コードの書き漏らしをコンパイル時に100%検知できる。
- 将来の拡張や未知のサブクラスに備える際は、既知のケースをしっかり列挙した上で、ワイルドカード(`_`)とヘルパー関数を組み合わせることで、「型安全性」と「実行時への柔軟性」を両立させることができる。
ここをクリアできれば、あなたのDartのコードは、保守性が高く、変更に強い「プロフェッショナルな設計」へと生まれ変わります。
ぜひ、日々のFlutterやDartの開発でこのパターンを試してみてくださいね。あなたのコードがより美しく、安全になることを応援しています!それではまた。