Overview & usage(模式概览与用法)
模式(pattern)用于描述值应具有的形状。Dart 可以用模式检查一个值是否符合预期,也可以在匹配成功后把值拆成更小的部分,并将这些部分绑定到变量。
模式从 Dart 3.0 开始可用。它尤其适合处理 record、集合、对象层级和外部数据:原本分散的类型检查、字段读取与变量声明,可以集中写在一个结构中。
void main() {
final response = (status: 200, body: 'OK');
if (response case (status: 200, body: final message)) {
print(message); // OK
}
}
这个 record pattern 同时完成了三件事:检查 status 是否为 200、读取 body 字段,并把字段值绑定到 message。
模式能做什么
模式主要有匹配和解构两种能力,同一个模式可以同时使用它们。
匹配值
匹配用于判断值是否符合模式。判断规则取决于模式种类,例如常量模式比较值是否相等,类型模式检查值的类型,列表模式检查列表及其元素的结构。
String classify(Object value) {
return switch (value) {
0 => 'zero',
int number when number > 0 => 'positive integer',
[_, _] => 'two-item list',
_ => 'other',
};
}
void main() {
print(classify(3)); // positive integer
print(classify(['Dart', 3])); // two-item list
}
0是常量模式,只匹配与0相等的值。int number检查类型,并在成功后把值绑定到number。[_, _]匹配恰好包含两个元素的列表,_表示忽略对应的值。- 最后的
_是通配符模式,用于接住其余情况。
复杂模式会递归匹配内部的子模式。[String name, int score] 不只要求值是两个元素的列表,还要求两个元素依次为 String 和 int。
解构值
解构用于从一个复合值中取出组成部分,并直接绑定到变量:
void main() {
final course = (
name: 'Dart',
progress: [8, 12],
);
final (name: courseName, progress: [completed, total]) = course;
print('$courseName: $completed/$total'); // Dart: 8/12
}
外层 record pattern 读取命名字段,内层 list pattern 再拆分列表。模式可以嵌套,因此不必先保存 course.progress,再逐项读取元素。
常见模式一览
下面这些模式可以单独使用,也可以互相嵌套:
| 模式 | 示例 | 作用 |
|---|---|---|
| 常量模式 | case 404: | 匹配与常量相等的值 |
| 变量模式 | case int code: | 检查类型并绑定变量 |
| 通配符模式 | _、String _ | 忽略值,或只检查类型 |
| record pattern | (var x, var y) | 按字段结构匹配或解构 record |
| list pattern | [first, second] | 按元素位置匹配或解构列表 |
| map pattern | {'name': String name} | 按键读取并匹配 Map 中的值 |
| object pattern | User(:name, :age) | 检查对象类型并调用 getter 解构属性 |
| 关系模式 | >= 0 && <= 100 | 使用关系和逻辑模式组合条件 |
| 或模式 | 'yes' || 'y' | 任一子模式匹配即可 |
| 空检查模式 | var value? | 只匹配非空值并绑定非空类型变量 |
本篇重点介绍模式的使用位置和整体思路;不同模式的完整语法可以在掌握这些场景后继续分别学习。
在变量声明中解构
局部变量声明可以在 var 或 final 后直接使用模式。右侧的值必须与模式匹配,因为声明没有“匹配失败后执行其他分支”的位置。
({String name, int lessons}) loadCourse() {
return (name: 'Dart', lessons: 12);
}
void main() {
final (:name, :lessons) = loadCourse();
print(name); // Dart
print(lessons); // 12
}
在命名 record pattern 中,:name 是 name: name 的简写:字段名和新变量名相同时,可以省略重复部分。
变量声明模式必须是不可反驳的(irrefutable),也就是对右侧的静态类型必然能够匹配。可能失败的类型检查或常量比较,应放在 if-case 或 switch 中处理。
给已有变量赋值
赋值模式不会创建变量,而是解构右侧的值并更新已经存在的变量。交换两个变量时不需要临时变量:
void main() {
var left = 'L';
var right = 'R';
(left, right) = (right, left);
print('$left$right'); // RL
}
模式赋值左侧只能绑定已经声明、并且可以重新赋值的变量。因此不能用它修改 final 变量。
使用 if-case 处理单个条件
if (value case pattern) 适合只关心一种数据形状的场景。模式匹配成功后,绑定的变量可以在 if 分支中使用:
void printUser(Object? data) {
if (data case {'user': [String name, int age]}) {
print('$name is $age years old.');
} else {
print('Invalid user data');
}
}
void main() {
printUser({
'user': ['Lily', 13],
}); // Lily is 13 years old.
}
这个 map pattern 会依次确认:值是 Map、包含键 'user'、该键对应一个长度为 2 的列表,并且元素类型依次为 String 和 int。任一步不匹配都会进入 else,不会执行不安全的类型转换。
Map 可以包含模式未列出的其他键;map pattern 只要求写出的键存在并且对应值匹配。相对地,不含 rest element 的 list pattern 默认要求列表长度与模式一致。
添加守卫条件
模式只负责结构和类型还不够时,可以使用 when 添加守卫(guard):
String describeScore(Object value) {
if (value case int score when score >= 60 && score <= 100) {
return 'passed: $score';
}
return 'not a passing score';
}
void main() {
print(describeScore(85)); // passed: 85
}
执行顺序是先匹配 int score,再计算 when 后的布尔表达式。守卫为 false 时,本次 case 视为未匹配。
在 switch 中处理多种形状
每个 case 本身就是一个模式。switch 表达式适合根据不同形状直接产生结果:
String formatResult(Object result) {
return switch (result) {
(200, String body) => 'success: $body',
(int code, _) when code >= 400 => 'error: $code',
{'loading': true} => 'loading',
_ => 'unknown',
};
}
void main() {
print(formatResult((200, 'Done'))); // success: Done
print(formatResult((404, null))); // error: 404
}
case 中绑定的变量只在对应分支内可见。模式按照书写顺序尝试,因此更具体的分支通常应放在更宽泛的分支之前。
配合 sealed class 做穷尽检查
模式与 sealed class 配合时,可以把一组相关类型的处理集中在一个穷尽的 switch 中:
sealed class Result {}
class Success extends Result {
Success(this.value);
final String value;
}
class Failure extends Result {
Failure(this.message);
final String message;
}
String describe(Result result) {
return switch (result) {
Success(value: final value) => 'success: $value',
Failure(message: final message) => 'failure: $message',
};
}
void main() {
print(describe(Success('saved'))); // success: saved
}
Success(...) 和 Failure(...) 是 object pattern。它们先检查对象类型,再通过对应 getter 取值。由于 Result 是 sealed class,分析器知道当前库中的直接子类型,可以检查 switch 是否覆盖所有可能情况。
在循环中解构元素
for-in 循环可以在每次迭代时直接解构元素。遍历 Map 时,entries 返回 MapEntry,可以使用 object pattern 取得键和值:
void main() {
final scores = {
'Alice': 92,
'Bob': 85,
};
for (final MapEntry(key: name, value: score) in scores.entries) {
print('$name: $score');
}
}
如果变量名与 getter 名相同,可以写成更短的形式:
void main() {
final scores = {'Alice': 92};
for (final MapEntry(:key, :value) in scores.entries) {
print('$key: $value');
}
}
这比在循环体中重复书写 entry.key 和 entry.value 更直接,也让元素应有的结构显示在循环头部。
在集合字面量中使用模式
集合的 control-flow element 也支持 if-case。只有匹配成功时,对应元素才会加入集合:
void main() {
Object response = (200, 'Ready');
final messages = <String>[
if (response case (200, String body)) body,
];
print(messages); // [Ready]
}
这种写法适合根据一个可选数据形状构造集合。如果判断逻辑较长或包含多个分支,先在普通控制流中完成匹配通常更易读。
选择合适的用法
模式不是为了消除所有属性访问,而是把“验证形状”和“取出数据”放在需要它们的位置。常见选择如下:
| 场景 | 推荐写法 |
|---|---|
| 已知 record 的形状并要声明多个变量 | final (a, b) = record; |
| 要更新多个已有变量 | (a, b) = (b, a); |
| 只处理一种可能的数据形状 | if (value case pattern) |
| 需要区分多个常量、类型或结构 | switch 模式 |
| 结构匹配后还要检查业务条件 | case pattern when condition |
| 循环元素本身是 record 或对象 | 在 for-in 头部使用模式 |
| 外部 JSON 需要结构和类型验证 | map、list 与类型模式嵌套 |
如果只是读取一次简单属性,普通的 object.property 往往更清楚。模式最有价值的场景是需要同时检查、拆分或绑定多个部分。
TypeScript 对比
TypeScript 支持数组和对象解构,可以类比 Dart 的部分解构能力:
const course = { name: 'TypeScript', lessons: 12 }
const { name, lessons } = course
但 TypeScript 目前没有与 Dart 语言模式完全对应的内置语法。TypeScript 的解构主要负责取值,不能像 Dart 的 if-case 或 switch 模式一样,在一个结构中递归完成常量匹配、运行时类型检查和变量绑定。处理联合类型时,TypeScript 通常依赖判别字段、if 或 switch 来缩小类型。
常见误区
把模式当作普通布尔表达式
模式不是独立返回 bool 的表达式,不能写成 if ([a, b])。需要在条件中匹配时,应使用 if (value case [var a, var b])。
在声明中使用可能失败的模式
变量声明必须保证模式能够匹配。外部数据的结构不可靠时,不要直接解构;使用 if-case 或 switch 为失败情况提供分支。
认为列表模式会忽略多余元素
[first, second] 只匹配长度为 2 的列表。需要接收其余元素时,应显式使用 rest element,例如 [first, ...rest]。
忘记守卫失败后仍会继续匹配
在 switch 中,某个 case 的模式成功但守卫为 false 时,Dart 会继续尝试后续 case。把条件写在 case 体内的 if 中则不会产生相同的分支选择行为。
小结
- 模式描述值的形状,可以匹配、解构并绑定变量。
- 变量声明和赋值模式用于已知结构;可能失败的匹配应使用
if-case或switch。 - 模式可以递归嵌套,适合处理 record、List、Map 和对象。
when守卫用于补充模式无法表达的业务条件。_可以忽略不关心的部分,list pattern 需要用 rest element 明确接收剩余元素。- 模式与 sealed class 搭配,可以让分析器检查
switch是否穷尽。