Dart - 认识Sealed

第一章:被 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 出现之前,我们面临着一个两难的选择:

  1. 选 Abstract Class :可以携带丰富的数据,但没有穷尽性检查,容易漏写分支,维护极其困难。
  2. 选 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(数据为空)的状态。"

  1. 你只需要去定义文件里加一行:
dart 复制代码
class Empty<T> extends ApiState<T> {}
  1. 保存文件的瞬间,奇迹发生了:
    IDE(VS Code 或 Android Studio)会立即把你项目中所有用到了 ApiStateswitch 代码标红。
  • 错误提示:The type 'ApiState' is not exhaustively matched.(ApiState 没有被穷尽匹配)
  1. 你不需要靠脑子记哪里用了这个状态,你只需要跟着编译器的红线走 ,把每一个标红的地方补上 case Empty(): ...
  2. 当你修完最后一个红线,你就可以自信地提交代码了。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 消费状态 = 永远不会漏掉逻辑的健壮代码。

相关推荐
2501_940007893 小时前
Flutter for OpenHarmony三国杀攻略App实战 - 鸿蒙适配与打包发布
前端·flutter
一起养小猫3 小时前
Flutter for OpenHarmony 进阶:数据统计与排序算法深度解析
flutter·harmonyos
gpldock2223 小时前
Flutter App Templates Deconstructed: A 2025 Architectural Review
开发语言·javascript·flutter·wordpress
微祎_4 小时前
Flutter for OpenHarmony:构建一个 Flutter 单词拼图游戏,深入解析状态驱动 UI、交互式字母操作与教育类应用设计
javascript·flutter·ui
一起养小猫4 小时前
Flutter for OpenHarmony 实战:文件加密解密器完整开发指南
flutter
linweidong4 小时前
大厂工程化实践:如何构建可运维、高稳定性的 Flutter 混合体系
javascript·flutter
一起养小猫5 小时前
Flutter for OpenHarmony 进阶:异步编程与同步机制深度解析
flutter·harmonyos
向哆哆5 小时前
Flutter × OpenHarmony 跨端开发:高校四六级报名管理系统中的“常见问题”模块实现解析
flutter·开源·鸿蒙·openharmony·开源鸿蒙
一起养小猫5 小时前
Flutter for OpenHarmony 进阶:搜索算法与数据持久化深度解析
flutter·harmonyos