Dart 3 的补货:记录、模式与 sealed
这条直觉在 2023 年之前是对的。Dart 3 一口气补齐了记录、模式匹配、sealed 类和详尽性检查——现在它不但补上了 Kotlin 的 sealed + when,某些地方还走得更远:Dart 的模式能一层层解构到底,而 Kotlin 的 when 只能判断类型然后自己手动取字段。这一章带一台真的匹配引擎,你改一个值它当场重判。
记录:终于能返回两个值了
Kotlin 有 Pair、Triple 和 data class。Dart 3 给的是记录(record)——一个轻量的、结构化的、有值语义的匿名聚合类型:
// 位置字段
(int, String) parse(String s) => (200, 'OK');
final r = parse('...');
print(r.$1); // 200,位置字段用 $1 $2 访问
print(r.$2); // OK
// 具名字段(推荐,可读性好得多)
({int code, String body}) fetch() => (code: 200, body: 'OK');
final res = fetch();
print(res.code);
三个关键点:
- 记录是值类型:
(1, 'a') == (1, 'a')为true,不用你写==。这一点比手写类省事得多。 - 类型就是结构:不用声明,写出来就有。两个函数返回
({int code, String body}),它们的类型是同一个。 - 但它不能有方法。需要行为就老老实实定义类。记录是「顺手打包几个值」,不是「省掉建模」。
用记录:函数要返回两三个值、内部临时打包、Future.wait 后拆结果。
用类:这个概念在业务里有名字(Course、Student)、要跨文件传、要序列化。把领域模型写成裸记录会在三个月后咬你——字段一多,({String a, String b, String c, String d}) 这种类型签名会失控。
模式:解构,而且能一层套一层
有了记录,自然就有了解构。Dart 3 把「模式」做成了一等公民,可以出现在变量声明、for 循环、switch 分支三个位置:
// 1. 声明处解构
final (code, body) = fetch();
final (:code, :body) = fetch(); // 具名字段的简写,等价于 (code: code, body: body)
// 2. for 循环里解构 Map
for (final MapEntry(:key, :value) in scores.entries) {
print('$key 得了 $value 分');
}
// 3. 交换两个变量,不用临时变量
(a, b) = (b, a);
第二条那个 MapEntry(:key, :value) 值得看两眼:它是对象模式——匹配一个 MapEntry,同时把它的 key 和 value getter 的返回值绑成同名局部变量。Kotlin 里遍历 Map 要写 for ((k, v) in map),靠的是 Map.Entry 上定义的 component1/component2 扩展;Dart 这套更通用——任何对象的任何 getter 都能在模式里解构,不需要类作者提前配合。
switch 表达式 + sealed:Kotlin 那套,一比一还给你
先看你熟悉的 Kotlin:
sealed interface Result
data class Ok(val body: String) : Result
data class Err(val code: Int) : Result
data object Loading : Result
val text = when (r) {
is Ok -> r.body // 智能转换后手动取字段
is Err -> "出错 ${r.code}"
Loading -> "加载中"
} // 少写一个分支 → 编译报错
Dart 3 的版本:
sealed class Result {}
class Ok extends Result { final String body; Ok(this.body); }
class Err extends Result { final int code; Err(this.code); }
class Loading extends Result {}
final text = switch (r) {
Ok(body: final b) => b, // 判类型 + 取字段,一步完成
Err(code: final c) => '出错 $c',
Loading() => '加载中',
}; // 少写一个分支 → 编译报错
逐条对应:sealed class 对应 sealed interface(都要求子类型在同一个库里,编译器才能穷举);switch 表达式对应 when 表达式;详尽性检查(exhaustiveness)也一样有——漏一个子类型,编译器当场报错,这正是 sealed 的全部意义。
不同之处在于那个 Ok(body: final b):Dart 在判断类型的同一行里就把字段掏出来了,而且能继续往下套:
// 一路解构到第三层,还带条件
final label = switch (event) {
Submit(form: Form(name: '', :final email)) => '名字空着,邮箱是 $email',
Submit(form: Form(:final name)) when name.length > 20 => '名字太长',
Submit() => '提交',
Cancel() => '取消',
};
这个能力在 Kotlin 里得写成一串嵌套 if + 手动取字段。「Dart 建模难受」这条直觉,可以彻底扔了。
模式一共有哪些
不用背,用到再查,但过一眼有好处:
| 模式 | 写法 | 含义 |
|---|---|---|
| 常量 | 200、'OK'、null | 用 == 比 |
| 变量 | final b、var b | 无条件匹配并绑定 |
| 通配 | _ | 匹配但不绑定(Dart 3.7 起 _ 是真正的通配符) |
| 类型 | int n、String _ | 类型判断 + 可选绑定 |
| 记录 | (200, var body) | 按位置/名字拆记录 |
| 列表 | [first, ...rest] | 按长度和元素匹配,... 吃掉中间 |
| Map | {'id': var id} | 按键取值再匹配(解析 JSON 很好用) |
| 对象 | Circle(radius: var r) | 类型判断 + 调 getter 解构 |
| 关系 | >= 400、< 0 | 比较运算,替代一堆 if |
| 逻辑 | 1 || 2 || 3、>0 && <10 | 或 / 与 |
| 守卫 | … when r > 10 | 匹配成功后再加一道布尔条件 |
亲手跑一跑:真的模式匹配求值器
下面这台不是示意图,是真的解析器加真的匹配器:它会把左边那些 case 逐条解析成模式树,再拿你输入的值去匹配,从上往下第一个成功的就是结果。改一改输入的值,看命中哪一条、绑定出了什么。
试几个有意思的:在「HTTP 响应」里把值改成 (404, 'Not Found')——它会跳过第一条(常量 200 不匹配),落到第二条的守卫 code >= 400 上。在「形状」里把半径从 3 改到 30,第一条的守卫就会翻过来。注意匹配是从上往下、第一个命中就停,所以带守卫的特例必须写在通用条的前面——这和 Kotlin 的 when 一样。
在真实 Flutter 代码里,你会在这两处用到它
一、异步三态
这是 Flutter 里模式匹配最舒服的场景。用 Riverpod 的 AsyncValue(第 16 章)时:
Widget build(BuildContext context, WidgetRef ref) {
final courses = ref.watch(courseListProvider);
return switch (courses) {
AsyncData(:final value) => CourseList(courses: value),
AsyncError(:final error) => ErrorView(message: '$error'),
_ => const Center(child: CircularProgressIndicator()),
};
}
三行覆盖加载中/成功/失败,而且编译器保证你没漏。对照一下你在 Compose 里写的 when (uiState) { is Loading -> …; is Success -> …; is Error -> … }——同一个东西。
二、手写 JSON 解析时的防御
Dart 没有运行时反射,从网络拿到的是 Map<String, dynamic>。Map 模式让「检查结构」和「取值」变成一步:
if (json case {'id': int id, 'name': String name, 'score': num score}) {
// 走到这里说明三个字段都在、类型都对,而且已经绑好了
return Student(id: id, name: name, score: score.toDouble());
}
throw const FormatException('返回格式不对');
那个 if (x case 模式) 叫 if-case 语句,是模式匹配的单分支版本,用来替代「先判空再强转再取值」的三行样板。不过日常项目还是该用代码生成(第 22 章),手写解析只适合边界少、字段少的地方。
顺手说说 Dart 3.6 之后的零碎更新
你上次认真看 Dart 大概是几年前,这几年攒了一批小东西,都很实用:
- 数字分隔符(3.6):
const oneBillion = 1_000_000_000; - 通配符变量(3.7):
_正式成为「我不要这个值」的写法,(_, final b) = pair;,多个_不冲突。 - 空安全元素(3.8):
[a, ?maybeNull, c]——值为 null 时这一项直接不出现在列表里,写 UI 时省掉一堆if (x != null)。 - 点简写(3.10 起稳定):上下文类型已知时可以省掉类型名——
Color c = .blue;、onPressed: () => setState(…), style: .filled()。Swift 用户会觉得眼熟,写枚举和主题常量时能省很多字。 - 私有具名参数(3.12,上一章讲过)与实验中的主构造函数。
if-case:单分支的模式匹配
不是每次都要 switch。当你只关心「一种情况」时,用 if (x case 模式) 更轻——它是模式匹配的单分支版,专门替代「先判类型/判空、再强转、再取值」这种三行样板:
// ✗ 老写法:三步
final data = response.data;
if (data is Map<String, dynamic> && data['user'] != null) {
final user = data['user'] as Map;
print(user['name']);
}
// ✓ if-case:一步,判断 + 解构 + 绑定同时完成
if (response.data case {'user': {'name': final name}}) {
print(name); // 走到这里说明结构对了,name 已经绑好、类型也对
}
它在 Flutter 里特别顺手的一个场景是从 AsyncValue 或路由参数里「有就取、没有就跳过」。if-case 还能带 else,以及 when 守卫:if (x case final n when n > 0) ...。
详尽性检查:sealed 真正的价值
第 16 章会让你用 AsyncValue(加载/成功/失败)和自己的 sealed 状态类。这里把「详尽性」为什么重要说透,因为它是 sealed 的全部意义所在:
sealed class SubmitState {}
class Idle extends SubmitState {}
class Submitting extends SubmitState {}
class Success extends SubmitState {}
class Failure extends SubmitState { final String msg; Failure(this.msg); }
Widget build(...) => switch (state) {
Idle() => const SubmitButton(),
Submitting() => const CircularProgressIndicator(),
Success() => const CheckMark(),
// ✗ 如果漏了 Failure,编译器直接报错:switch 不详尽
};
这就是价值所在:哪天你给状态机加了一个新分支(比如加个 Cancelled 状态),编译器会把项目里每一个处理这个状态的 switch 都标红——逼你去补上新分支的 UI,一个都漏不掉。这是「用类型系统保证你没忘事」的典型,也是把 UI 状态建成 sealed(而不是几个散落的 bool)的最大理由。你在 Kotlin 里靠 when + sealed 得到的这份安全感,Dart 3 一模一样给你。
Dart 里这两个是不同的东西,语法细节也不同,写混了编译器的报错还挺难懂:
// switch 语句:分支用冒号,语句块,需要 break(模式分支不需要 break)
switch (r) {
case Ok(:final body):
print(body);
case Err():
print('err');
}
// switch 表达式:分支用 =>,用逗号分隔,整体是一个值,末尾有分号
final text = switch (r) {
Ok(:final body) => body,
Err() => 'err',
};
另外:只有 sealed 类型才有详尽性检查。对 int、String 这种开放类型做 switch 表达式,编译器会要求你写一条兜底的 _ => …。
一比一还给你的:sealed interface → sealed class,when 表达式 → switch 表达式,详尽性检查 → 详尽性检查,Pair → 记录。你在 Compose 里那套「UI 状态用 sealed 建模,界面用 when 分发」的架构,可以原样搬过来。
比 Kotlin 强的:模式能一层层解构到底(Submit(form: Form(:final name))),Kotlin 的 when 只能判类型再手动取;列表模式、Map 模式、关系模式 Kotlin 都没有。
还缺的:没有 data class 自动生成 copy——所以状态类还是要靠 freezed(第 22 章)。
这一章的一句话
Dart 3 已经不是你印象里那门「建模难受」的语言:记录、模式、sealed 与详尽性检查全都到位,而且模式能一层解构到底——你在 Compose 里用 sealed + when 建模 UI 状态的整套习惯,可以一字不改地搬过来。
语言部分到这里就够用了。下一章我们讲一个只有一个单词的话题:const。在别的语言里它是个可有可无的修饰符,在 Flutter 里它是一个性能开关——而理解它为什么是开关,正好是打开卷 II 那三棵树的钥匙。