当了解sealed class的模式匹配能力的时候,我第一想到的是枚举。
在 Dart 中 enum 和 sealed class 都可以用于建模有限类型集合,但用途和灵活性有显著区别。
🔶 1. 使用场景对比
| 特性 / 对比点 | enum(增强枚举) | sealed class(密封类) |
|---|---|---|
| 用于建模有限状态 | ✅ 非常适合(内建支持 values 等) | ✅ 更灵活但需手动处理 |
| 可携带不同字段 | ⚠️ 只能有一个构造函数,所有枚举项字段一样 | ✅ 各子类可有完全不同的字段 |
| 模式匹配 | ✅ Dart 3 支持 switch exhaustiveness | ✅ Dart 3 switch 也完全支持 |
| 扩展性 | ❌ 不支持继承枚举,不能新增项 | ✅ 可以新增子类 |
| 方法和 getter | ✅ 支持 | ✅ 支持 |
| 类型安全 & 约束 | ✅ 限定值集合清晰 | ✅ 更强表达能力 |
| 简洁性 | ✅ 非常简洁 | ❌ 需要更多代码 |
| 推荐用法 | ✅ 状态、颜色、固定类型等 | ✅ 多状态对象、响应类型、操作结果等 |
🔹 2. 示例对比:状态建模
✅ 使用 enum(适合状态固定,无需复杂数据)
enum ConnectionState {
connected,
disconnected,
connecting,
}
✅ 使用 sealed class(更复杂状态,携带数据)
sealed class ConnectionState {}
class Connected extends ConnectionState {
final DateTime timestamp;
Connected(this.timestamp);
}
class Disconnected extends ConnectionState {}
class Connecting extends ConnectionState {
final int retryCount;
Connecting(this.retryCount);
}
🔸 模式匹配(Dart 3)
void handle(ConnectionState state) {
switch (state) {
case Connected():
print('已连接');
break;
case Connecting(:var retryCount):
print('重试次数: $retryCount');
break;
case Disconnected():
print('已断开');
break;
}
}
🔹 3. 密封类的关联值优势
Dart 的增强枚举(Enhanced Enums,Dart 2.17+)允许为每个枚举值定义静态且统一的属性,但这些属性在编译时固定,无法动态扩展或差异化。例如:
enum DeviceType {
lamp(value: 1, icon: 'lamp_icon'),
airConditioner(value: 2, icon: 'ac_icon');
final int value;
final String icon;
const DeviceType({required this.value, required this.icon});
}
特点:
- 静态属性:所有枚举成员必须定义相同的属性(如 value 和 icon),且类型一致。
- 不可扩展:无法为不同枚举成员添加不同的方法或动态属性。
- 通过扩展函数补充逻辑:需借助扩展函数实现额外行为(如转换字符串)
密封类的子类可以自由定义属性和方法,每个子类可独立携带不同数据,且支持动态扩展:
sealed class Result {}
class Success implements Result {
final String data;
Success(this.data);
}
class Error implements Result {
final int code;
final String message;
Error(this.code, this.message);
}
特点:
- 动态属性:每个子类可定义独立的属性(如 Success 只有 data,Error 有 code 和 message)。 - 方法扩展:子类可包含独立的方法逻辑。 - 编译时类型安全:switch 表达式强制覆盖所有子类分支
具体场景对比
场景 1:表示 API 错误
枚举实现(静态属性):
enum ApiErrorCode {
network(code: 1001, message: '网络错误'),
server(code: 5001, message: '服务器错误');
final int code;
final String message;
const ApiErrorCode({required this.code, required this.message});
}
缺点:所有错误类型必须遵循相同的属性结构,无法为特定错误添加额外字段(如 details)。
密封类实现(动态属性):
sealed class ApiError {}
class NetworkError implements ApiError {
final int code;
final String message;
final String details; // 额外字段
NetworkError(this.code, this.message, this.details);
}
class ServerError implements ApiError {
final int code;
ServerError(this.code);
}
优势:NetworkError 可携带 details,而 ServerError 无需冗余字段。
场景 2:状态管理
枚举实现(静态属性):
enum LoadingState {
dataLoaded(data: '默认数据'),
error(message: '默认错误');
final String data;
final String message;
const LoadingState({required this.data, required this.message});
}
缺点:所有状态必须包含 data 和 message,即使某些状态不需要(如纯加载中状态)。
密封类实现(动态属性):
sealed class LoadingState {}
class DataLoaded implements LoadingState {
final String data;
DataLoaded(this.data);
}
class Loading implements LoadingState {}
class ErrorState implements LoadingState {
final String message;
ErrorState(this.message);
}
优势:状态可按需携带数据,避免冗余。
总结
-
枚举关联值的局限性
- 属性必须统一,无法差异化定义。
- 无法为特定枚举值添加独立方法或逻辑。
- 依赖扩展函数补充功能,代码分散。
-
密封类的优势
- 子类可自由定义属性和方法,支持动态扩展。
- 编译时强制类型检查,避免分支遗漏。
- 代码结构更清晰,适合复杂业务场景。
🔶 4. 本质差异
-
设计目标
- 枚举:用于表示固定且无状态的常量集合(如星期、方向)。
- 密封类:用于表示有限但可扩展的类型集合,且每个类型可独立定义行为和状态。
-
扩展性
- 枚举无法继承或扩展,新增类型需修改原枚举定义。
- 密封类允许通过继承添加新类型,且编译器自动识别新类型(需在同一库)。
-
代码可读性
- 密封类通过子类名称直接表达意图(如
NetworkError比ErrorType.network更清晰)。 - 枚举依赖附加值或额外方法解释状态细节。
- 密封类通过子类名称直接表达意图(如
✅ 5. 何时选择密封类而非枚举?
- 需要状态或行为差异:当每个类型需要携带不同数据或实现不同逻辑时。
- 严格的类型检查:依赖编译时强制覆盖所有分支的场景(如状态机)。
- 业务逻辑复杂度:涉及多层级状态或错误类型时,密封类能保持代码简洁和可维护性。
密封类通过类型安全的模式匹配、灵活的状态扩展和编译时检查,解决了枚举在复杂场景下的局限性。在 Flutter 开发中,密封类尤其适合用于状态管理、错误处理等需要明确类型分支的场景,而枚举更适合简单的常量集合。两者的选择需根据具体业务需求权衡。