第一章:被 switch-case 坑过的"未知状态"
如果说 Records 解决的是"数据怎么传"的问题,那么 Sealed Classes 解决的就是**"逻辑怎么写"**的问题。
在 App 开发(尤其是 Flutter 开发)中,我们每天都在处理各种"状态":
- 网络请求:
Loading,Success,Error - 登录流程:
Unauthenticated,Authenticating,Authenticated - 支付结果:
Pending,Paid,Failed
在 Dart 3 之前,我们通常使用 abstract class(抽象类)或者 enum(枚举)来模拟这些状态。但坦白说,它们总感觉缺了点什么,就像是一个漏风的屋子,总有 Bug 能钻进来。
痛点一:永远不放心的 switch (编译器无法兜底)
这是最让人头秃的问题。当我们用普通的抽象类来定义状态体系时,编译器并不知道你到底有多少个子类。
场景还原:
假设你定义了一个用于处理订单状态的抽象类:
dart
// 普通抽象类
abstract class OrderState {}
class OrderCreated extends OrderState {}
class OrderPaid extends OrderState {}
class OrderShipped extends OrderState {}
当你编写 UI 逻辑来展示不同状态时,通常会用到 switch 或者 if-else:
dart
String getStatusText(OrderState state) {
if (state is OrderCreated) return '订单已创建';
if (state is OrderPaid) return '支付成功';
// ⛔️ 痛点:如果我这里疏忽了,忘了写 OrderShipped 的判断...
// 编译器完全不会报错!它觉得没问题。
// 结果:代码运行时可能直接返回 null,或者根本没有反应。
return '未知状态';
}
或者用 switch:
dart
switch (state.runtimeType) {
case OrderCreated: ...
case OrderPaid: ...
// 漏写了 OrderShipped,编译器依然沉默。
}
为什么痛苦?
- 没有安全感:每次写状态判断逻辑,你都得小心翼翼地去翻看定义文件,生怕漏了一个子类。
- 运行时炸弹:这种"遗漏"往往在写代码时发现不了,只有等到用户真的进入那个特定状态(比如发货了),发现界面一片空白或者报错,Bug 才会暴露。
痛点二:新增状态时的"全网追杀" (维护成本高)
即使你第一次写的时候很小心,覆盖了所有情况。但软件是会演进的。
场景还原:
三个月后,产品经理跑来说:"我们需要加一个 OrderRefunding (退款中) 的状态。"
于是你兴冲冲地加了一个类:
dart
class OrderRefunding extends OrderState {}
灾难开始了:
因为之前的代码(比如上面的 getStatusText)对这个新状态一无所知,且编译器也不会提醒你。
你必须凭借记忆,或者使用全局搜索(Ctrl+Shift+F),找出项目中所有用到了 OrderState 的地方,一个个手动补上 if (state is OrderRefunding)。
为什么痛苦?
- 人工排查太累:对于大型项目,这种状态可能在几十个文件中被用到。漏改一处,就是一个 Bug。
- 心智负担重:每次改状态定义,都像是在做外科手术,生怕切断了哪根神经。
痛点三:Enum 的"先天不足" (无法携带复杂数据)
有些开发者会说:"那我不使类了,我用 Enum (枚举) 行不行?Enum 在 switch 里是可以检查遗漏的呀。"
dart
enum OrderStatus { created, paid, shipped }
没错,Enum 解决了"检查遗漏"的问题。但它有一个致命弱点:所有枚举值长得都一样。
场景还原:
created状态可能只需要一个时间戳。paid状态需要一个transactionId(交易流水号)。shipped状态需要trackingNumber(快递单号) 和courierName(快递公司)。
普通的 Enum 做不到这一点。你只能把所有字段都塞进一个大类里,搞出一堆可空属性:
dart
class OrderState {
final OrderStatus status;
final String? transactionId; // 只有 paid 时才有用,但也得定义在这
final String? trackingNumber; // 只有 shipped 时才有用
// ...
}
为什么痛苦?
- 数据混乱 :当状态是
created时,你居然能访问到trackingNumber(虽然是 null),这在逻辑上是不合理的。 - 代码脏 :到处都是
if (state.transactionId != null)这样的防御性代码。
总结
在 Sealed Classes 出现之前,我们面临着一个两难的选择:
- 选 Abstract Class :可以携带丰富的数据,但没有穷尽性检查,容易漏写分支,维护极其困难。
- 选 Enum :有穷尽性检查,但这货带不了复杂数据,只能处理简单逻辑。
"我全都要!"
我们要一种既能像 Class 一样携带复杂结构数据,又能像 Enum 一样被编译器严格监管的机制。
Sealed Classes (密封类) 正是为此而生。它就像是给你的状态机装上了一把严密的"安全锁",一旦开启,任何逻辑漏洞都逃不过编译器的眼睛。
第二章:Sealed 与 Switch ------ 命中注定的"原生 CP"
在了解了 Sealed Classes 能把子类"封死"在一个文件里之后,你可能会问:"这对写代码到底有什么实际好处?"
如果你只是用 if-else 去判断状态,那 Sealed Class 和普通的抽象类几乎没有区别,你依然会漏写逻辑,依然会写出 Bug。
Sealed Classes 的真正威力,只有在遇上 Dart 3 的 Switch 时才会彻底爆发。在实际开发中,90% 的 Sealed Class 都是配合 Switch 使用的。
1. 为什么它是"原生 CP"?(穷尽性检查)
Dart 编译器对 Sealed Class 有一种特殊的处理逻辑,叫做 Exhaustiveness Checking (穷尽性检查)。
- 对于普通类 :编译器不知道有多少个子类,所以它不关心你的
switch是否写完了所有情况(通常需要写default来兜底)。 - 对于 Sealed 类 :编译器完全知道 只有那几个子类。因此,它会强制 你的
switch必须覆盖每一个子类。少一个,它都不让你编译通过。
这把"运行时可能发生的崩溃",变成了"编译时就能看到的红线"。
2. 实战演练:告别冗长的 if-else
让我们看一个最经典的 API 请求状态管理场景。
第一步:定义 Sealed 状态(锁)
dart
// 定义锁:状态只有这三种,不可能有别的
sealed class ApiState<T> {}
class Loading<T> extends ApiState<T> {}
class Success<T> extends ApiState<T> {
final T data;
Success(this.data);
}
class Error<T> extends ApiState<T> {
final int code;
final String message;
Error(this.code, this.message);
}
第二步:使用 Switch 表达式(钥匙)
在 Dart 3 之前,我们写 switch 语句很啰嗦(需要 break,只能作为语句)。现在,我们有 Switch Expression (Switch 表达式),它可以直接返回结果,是构建 UI 的绝配。
dart
Widget buildPage(ApiState<String> state) {
// 这里的 switch 直接作为一个表达式返回 Widget
return switch (state) {
// 1. Loading 状态
Loading() => const CircularProgressIndicator(),
// 2. Success 状态 (同时使用了模式匹配解构数据)
// 注意:这里不需要手动把 state 强转为 Success,
// 语法直接把 data 提出来了!
Success(data: var content) => Text('内容: $content'),
// 3. Error 状态
Error(message: var msg) => Text('出错了: $msg'),
// 重点:完全不需要 default 分支!
// 因为编译器知道只有这三种情况。
};
}
3. "报错驱动开发" ------ 一种超爽的体验
Sealed + Switch 最爽的时刻,发生在你需要修改需求的时候。
假设产品经理跑来说:"我们需要增加一个 Empty(数据为空)的状态。"
- 你只需要去定义文件里加一行:
dart
class Empty<T> extends ApiState<T> {}
- 保存文件的瞬间,奇迹发生了:
IDE(VS Code 或 Android Studio)会立即把你项目中所有用到了ApiState的switch代码标红。
- 错误提示:The type 'ApiState' is not exhaustively matched.(ApiState 没有被穷尽匹配)
- 你不需要靠脑子记哪里用了这个状态,你只需要跟着编译器的红线走 ,把每一个标红的地方补上
case Empty(): ...。 - 当你修完最后一个红线,你就可以自信地提交代码了。0 Bug 风险。
4. 配合模式匹配 (Pattern Matching) 的二重奏
细心的你可能发现了,上面的代码中并没有出现 state.data 或者 state.message 的访问方式。
这是 Sealed + Switch 的另一个杀手锏:自动解包。
- 旧写法 (笨重):
dart
if (state is Success) {
// 必须手动强转,或者依赖编译器的类型推断
print((state as Success).data);
}
- 新写法 (优雅):
dart
// 在判断类型的"同时",把里面的数据拿出来赋值给变量 d
Success(data: var d) => print(d),
总结
Sealed Classes 本身只是一个规则定义(限制继承),但它存在的意义是为了让 Switch 能够完美工作。
- 没有 Switch 的 Sealed:就像一个普通的抽象类,依然充满隐患。
- Sealed + Switch:构成了 Dart 3 中最坚固的逻辑防线。
所以,记下这个公式:
Sealed 定义状态 + Switch 消费状态 = 永远不会漏掉逻辑的健壮代码。