はじめに
Dart 3では、「sealed class」 が正式に導入されました。
一方で、Dart 2.17 以降では「拡張列挙(Enhanced Enums)」も強化されています。
この2つはどちらも「型安全な状態表現」に使われますが、
目的・設計思想・使いどころが異なります。
本記事では、Flutterやクリーンアーキテクチャでも役立つように、
両者の違いと共通点を整理します。
1. 前提:どちらも「型安全な状態表現」のための仕組み
| 用語 | 概要 |
|---|---|
| 拡張列挙(Enhanced Enum) |
enum が強化され、プロパティ・メソッド・コンストラクタを持てるようになった。有限集合を安全に表現。 |
| sealed class | クラス階層の「外部継承を禁止」して、限定された継承パターンを型安全に扱うための新機能。 |
どちらも「状態を明示的に表す」ために使われますが、
enumは“値の集合”、**sealed classは“型の集合”**を表します。
2. 拡張列挙(Enhanced Enums)のおさらい
例:状態+データを持つ列挙型
enum ConnectionState {
connected('接続中'),
disconnected('未接続'),
error('エラー');
final String label;
const ConnectionState(this.label);
bool get isConnected => this == ConnectionState.connected;
}
特徴
- 定数の集合(有限)
- プロパティやメソッドを持てる
- すべての値がコンパイル時に確定している
-
switch文で網羅チェックが可能(exhaustive)
String message(ConnectionState state) => switch (state) {
ConnectionState.connected => '通信中...',
ConnectionState.disconnected => 'オフライン',
ConnectionState.error => '通信エラー',
};
3. sealed class:クラス階層を「安全に封じる」
sealed class は「そのファイル内でしか継承できないクラス」です。
Dart 3で正式に導入されました。
sealed class ApiResponse {}
class Success extends ApiResponse {
final String data;
Success(this.data);
}
class Failure extends ApiResponse {
final String message;
Failure(this.message);
}
sealedを付けることで、
ApiResponseを他のファイルから継承できなくなります(安全なクラス階層)。
switch と exhaustiveness(網羅性チェック)
Dart 3 では、sealed class を使うと switch ですべてのサブタイプを網羅しているかチェックしてくれます。
String handle(ApiResponse res) => switch (res) {
Success(:var data) => 'OK: $data',
Failure(:var message) => 'NG: $message',
};
もし Success または Failure のどちらかを忘れると、コンパイルエラーになります。
→ これにより「未処理の状態」を防げます。
4. enum と sealed class の違い(概要表)
| 比較項目 | 拡張列挙(Enhanced Enum) | sealed class |
|---|---|---|
| 定義対象 | 定数の集合 | 型(クラス)の集合 |
| 継承構造 | 単一の列挙型 | サブクラス階層 |
| 値の追加 | 列挙値で固定 | サブクラス追加で拡張 |
| プロパティ | 定義できる(固定値) | 自由に定義できる(任意構造) |
switch 網羅チェック |
あり | あり(Dart 3) |
| ジェネリクス対応 | ❌(一部制限あり) | ✅ |
| 外部継承 | ❌ 不可 | ❌(sealedにより禁止) |
| 主な用途 | 状態・モードの表現 | 状態・結果・イベントモデルの表現 |
| 例 |
ThemeMode, HttpStatus, etc. |
ApiResponse, UiState, etc. |
5. どちらを使うべき?実践シナリオで比較
状態が「固定された有限集合」なら → 拡張列挙
例:アプリのテーマ、接続状態、モード切り替え
enum ThemeMode {
light('明るい'),
dark('暗い');
final String label;
const ThemeMode(this.label);
}
→ 列挙が増える予定がない場合に最適。
状態に「異なるデータ構造」を持たせたいなら → sealed class
例:APIレスポンス、UIの状態、エラーハンドリング
sealed class Result<T> {}
class Success<T> extends Result<T> {
final T data;
Success(this.data);
}
class Failure<T> extends Result<T> {
final String message;
Failure(this.message);
}
String handle(Result<String> result) => switch (result) {
Success(:var data) => '成功: $data',
Failure(:var message) => '失敗: $message',
};
→ サブクラスごとにデータ構造を変えられるのが強み。
6. 両者の共通点
| 共通点 | 説明 |
|---|---|
| 型安全 | switch 式で網羅チェックが働く |
| 不変設計に適する |
const / final 構造を前提にできる |
| 状態モデルに強い | Flutter の UI State、Result、Event 表現などに最適 |
| 他言語でいう「代数的データ型(Algebraic Data Type)」に近い |
7. Flutterでの実践:状態管理モデルへの応用
enum 版(固定UI状態)
enum UiStatus { idle, loading, success, error }
Widget build(BuildContext context) {
switch (viewModel.status) {
case UiStatus.loading:
return const CircularProgressIndicator();
case UiStatus.success:
return const Text('完了!');
case UiStatus.error:
return const Text('エラー');
default:
return const SizedBox();
}
}
sealed class 版(データ付き状態)
sealed class UiState {}
class Loading extends UiState {}
class Success extends UiState {
final String message;
Success(this.message);
}
class Error extends UiState {
final String reason;
Error(this.reason);
}
Widget build(BuildContext context) {
return switch (viewModel.state) {
Loading() => const CircularProgressIndicator(),
Success(:var message) => Text(message),
Error(:var reason) => Text('失敗: $reason'),
};
}
→ sealed class により、状態とデータを一元的に管理できます。
8. 一歩深く:enum から sealed class に移行する目安
| 条件 | おすすめ |
|---|---|
| 定数的なモードや状態だけ | enum |
| 各状態で別のデータを保持したい | sealed class |
| 将来サブタイプが増える可能性がある | sealed class |
| 全ての状態が確定しており拡張不要 | enum |
まとめ
| 観点 | 拡張列挙 | sealed class |
|---|---|---|
| 目的 | 固定された定数を表す | 拡張可能な型階層を表す |
| 表現 | 値(インスタンス) | 型(クラス) |
| 拡張性 | 固定 | 柔軟 |
| switch 網羅性 | ✅ | ✅ |
| 実用例 | ThemeMode, HttpMethod | ApiResponse, UiState, Result |