Pattern types(模式类型)
Dart 提供多种模式类型,用于检查值、绑定变量和解构数据。每种模式只描述一条匹配规则;把它们嵌套或用逻辑模式组合后,就能同时检查一个值的类型、结构与内容。
模式从 Dart 3.0 开始可用。本文关注每种模式的语法与匹配规则;模式可以出现在哪些位置,参见模式概览与用法。
String describe(Object data) {
return switch (data) {
{'name': String name, 'scores': [int _ && >= 60, ...]} => '$name passed',
{'name': String name, 'scores': List<int> _} => '$name did not pass',
_ => 'invalid data',
};
}
void main() {
print(describe({
'name': 'Mia',
'scores': [82, 76],
})); // Mia passed
}
外层是 map pattern,String name 是带类型的变量模式,[int _ && >= 60, ...] 则组合了 list pattern、带类型通配符、关系模式和 rest element。模式会从外到内递归匹配,任一子模式失败,整个模式就失败。
模式类型一览
| 类别 | 模式类型 | 示例 | 主要作用 |
|---|---|---|---|
| 组合与分组 | 逻辑或、逻辑与、括号 | 0 || 1、>= 0 && <= 9 | 组合规则或改变优先级 |
| 检查与绑定 | 关系、常量、变量、通配符 | >= 18、true、String s | 检查值,并按需绑定变量 |
| 后缀检查 | 类型转换、空检查、空断言 | value as int、value? | 在继续匹配前转换或检查子模式的值 |
| 结构解构 | List、Map、Record、Object 模式 | [a, b]、Point(:x, :y) | 按数据结构递归匹配内部值 |
模式有优先级。大体上,逻辑或 || 的优先级低于逻辑与 &&,逻辑与又低于关系模式;不确定组合顺序时,应使用括号模式明确表达意图。
组合与分组模式
逻辑或模式
逻辑或模式 pattern1 || pattern2 从左到右尝试子模式,只要一个分支匹配就成功,后面的分支不再求值:
bool isPrimaryColor(String color) {
return switch (color) {
'red' || 'yellow' || 'blue' => true,
_ => false,
};
}
void main() {
print(isPrimaryColor('yellow')); // true
}
如果分支绑定变量,每个分支必须声明同一组变量。因为匹配成功后,后续代码不能依赖只存在于某个分支的变量。
逻辑与模式
逻辑与模式 pattern1 && pattern2 要求两个子模式都匹配。左侧失败时,右侧不会继续匹配:
String classify(int score) {
return switch (score) {
>= 0 && <= 59 => 'failed',
>= 60 && <= 100 => 'passed',
_ => 'invalid',
};
}
void main() {
print(classify(85)); // passed
}
两个子模式都会成功时,它们绑定的变量名不能重复,否则同一个变量会被绑定两次。
括号模式
括号模式 (subpattern) 用于改变组合模式的优先级。下面两个 case 的含义不同:
String check(bool value) {
const yes = true;
const no = false;
return switch (value) {
(yes || no) && yes => 'matches true only',
_ => 'no match',
};
}
void main() {
print(check(true)); // matches true only
}
在没有括号时,a || b && c 会按照 a || (b && c) 解释。括号模式只控制匹配顺序,不会创建 record;record pattern 至少要有两个位置字段,或者包含命名字段。
检查与绑定模式
关系模式
关系模式使用 ==、!=、<、>、<= 或 >=,将待匹配值与常量比较。它经常与逻辑与模式一起表达数值区间:
String describeTemperature(num value) {
return switch (value) {
< 0 => 'freezing',
>= 0 && < 30 => 'moderate',
_ => 'hot',
};
}
void main() {
print(describeTemperature(18)); // moderate
}
关系模式调用对应的运算符,因此待匹配值必须支持该比较。复杂的业务条件不适合塞进关系模式时,可以先绑定变量,再使用 when 守卫。
常量模式
常量模式在待匹配值与指定常量相等时成功。数字、布尔值、字符串、null、命名常量和常量构造对象都可以作为常量模式:
const retryCode = 503;
String statusMessage(int? code) {
return switch (code) {
200 => 'ok',
retryCode => 'retry later',
null => 'no response',
_ => 'unknown',
};
}
void main() {
print(statusMessage(retryCode)); // retry later
}
List 和 Map 的字面量默认会被解释为解构模式。要匹配整个常量集合,需要显式添加 const,例如 case const [1, 2]:。更复杂的常量表达式也要写成 const (expression)。
变量模式
变量模式在匹配成功后把值绑定到新变量。var name 和 final name 接受值的静态类型;String name 这样的带类型变量模式还会执行运行时类型检查:
String readName(Object value) {
return switch (value) {
{'name': String name} => name,
_ => 'anonymous',
};
}
void main() {
print(readName({'name': 'Lina'})); // Lina
print(readName({'name': 42})); // anonymous
}
变量只在确定该模式已经匹配的区域内可见。例如,switch case 中声明的 name 不能在其他 case 中使用。
标识符与通配符模式
单独出现的标识符会根据上下文表示不同含义:
| 上下文 | 示例 | 含义 |
|---|---|---|
| 声明 | var (a, b) = record; | 声明并绑定新变量 |
| 赋值 | (a, b) = record; | 给已有变量赋值 |
| 匹配 | case savedCode: | 匹配同名的已声明常量 |
| 任意 | _ | 匹配但忽略该位置的值 |
通配符 _ 不会声明变量。带类型的通配符还能只检查类型而不保留值:
bool hasExpectedTypes(Object value) {
return switch (value) {
(int _, String _) => true,
_ => false,
};
}
void main() {
print(hasExpectedTypes((1, 'Dart'))); // true
}
后缀检查模式
类型转换模式
类型转换模式 subpattern as Type 先把值强制转换为指定类型,再交给内部模式。转换失败会抛出异常,而不是让当前 case 匹配失败:
void main() {
(num, Object) data = (1, 'Dart');
final (count as int, label as String) = data;
print('$label: $count'); // Dart: 1
}
如果类型不确定且希望匹配失败后尝试其他分支,应使用带类型的变量模式,例如 case int count:;只有确信类型正确并希望错误立即暴露时,才使用类型转换模式。
空检查模式
空检查模式 subpattern? 先确认值不是 null,再匹配内部模式。遇到 null 时只会匹配失败,不会抛出异常;绑定变量的类型会提升为非空类型:
String greet(String? name) {
return switch (name) {
var value? => 'Hello, $value',
null => 'Hello, guest',
};
}
void main() {
print(greet('Mia')); // Hello, Mia
print(greet(null)); // Hello, guest
}
空断言模式
空断言模式 subpattern! 同样在非空值上继续匹配,并把绑定变量提升为非空类型;区别是遇到 null 会抛出异常:
void main() {
(int?, int?) point = (10, 20);
final (x!, y!) = point;
print('$x, $y'); // 10, 20
}
选择方式可以概括为:? 表示“为空就不匹配”,! 表示“为空就是程序状态错误”。处理外部输入时通常优先使用 ?,让失败进入明确的分支。
结构解构模式
List 模式与 rest element
List 模式 [subpattern1, subpattern2] 先检查值是否实现 List,再按位置递归匹配元素。没有 rest element 时,列表长度必须与模式完全相同:
String describeList(Object value) {
return switch (value) {
[] => 'empty',
[var only] => 'one item: $only',
[var first, var second] => 'two items: $first, $second',
_ => 'another shape',
};
}
void main() {
print(describeList([1, 2])); // two items: 1, 2
}
一个 List 模式最多包含一个 rest element ...,用于接收任意数量的中间元素。只写 ... 会忽略这些元素,写成 ...rest 则会把它们收集到一个新列表:
void main() {
final [first, ...middle, last] = [1, 2, 3, 4, 5];
print(first); // 1
print(middle); // [2, 3, 4]
print(last); // 5
}
Map 模式
Map 模式按键查找并递归匹配对应值。它只要求模式中写出的键存在,不关心 Map 是否还有其他键:
String readUser(Object data) {
return switch (data) {
{'id': int id, 'name': String name} => '$id: $name',
_ => 'invalid user',
};
}
void main() {
print(readUser({
'id': 7,
'name': 'Mia',
'active': true,
})); // 7: Mia
}
额外的 'active' 键不会影响匹配。Map 模式不支持 rest element,因为未写出的键本来就会被忽略。在 if-case 或 switch 等可失败的匹配中,缺少键会让模式失败;在没有失败分支的声明模式中,读取不存在的键会抛出 StateError。
Record 模式
Record 模式要求值与模式具有相同形状,也就是位置字段数量以及命名字段名称都一致,然后逐个匹配字段:
String formatCourse(({String name, int lessons}) course) {
final (:name, :lessons) = course;
return '$name: $lessons lessons';
}
void main() {
print(formatCourse((name: 'Dart', lessons: 12)));
// Dart: 12 lessons
}
命名字段中的 :name 是 name: name 的简写。带类型时也可以写成 :String name。Record 模式必须覆盖整个 record,不能像 Map 模式一样省略部分字段。
Object 模式
Object 模式 Type(property: subpattern) 先检查对象类型,再调用列出的 getter 解构属性。对象可以有模式未列出的其他属性:
class Point {
const Point(this.x, this.y);
final int x;
final int y;
}
String locate(Object value) {
return switch (value) {
Point(x: 0, y: 0) => 'origin',
Point(:var x, :var y) => '($x, $y)',
_ => 'not a point',
};
}
void main() {
print(locate(const Point(3, 4))); // (3, 4)
}
Point(:var x, :var y) 分别是 Point(x: var x, y: var y) 的简写。Object 模式调用 getter,不是直接访问对象的内部字段,因此也可以解构计算属性。
嵌套模式的匹配顺序
阅读复杂模式时,可以按照从外到内、从左到右的顺序拆解:
String parse(Object data) {
return switch (data) {
{
'user': (id: int id, name: String name?),
'roles': ['admin' || 'editor', ...],
} => '$id: $name',
_ => 'invalid',
};
}
void main() {
print(parse({
'user': (id: 7, name: 'Mia'),
'roles': ['admin', 'reviewer'],
})); // 7: Mia
}
这个模式依次检查:
- 外层值是包含
'user'和'roles'键的 Map; 'user'是指定形状的 record,id是int,name是非空String;'roles'是非空 List,首项为'admin'或'editor';- 所有条件成立后,
id和name才能在 case 结果中使用。
TypeScript 对比
TypeScript 的数组和对象解构可以类比 Dart 的 List、Record 与 Object 模式,但主要作用是取值:
const user = { id: 7, name: 'Mia' }
const { id, name } = user
Dart 模式还可以在同一个结构中执行运行时类型检查、常量匹配、关系比较和逻辑组合。TypeScript 没有与 Dart if-case、关系模式或 object pattern 完全对应的内置语法,通常需要解构后再配合 if、类型守卫或判别联合完成判断。
常见误区
混淆空检查与空断言
value? 把 null 当作普通的匹配失败,value! 则在遇到 null 时抛出异常。不能只因为 ! 写法更短就用它处理不可信数据。
认为所有结构模式都允许省略内容
Map 和 Object 模式只检查列出的键或 getter;List 和 Record 模式默认要求完整形状。List 需要通过 ... 显式容纳额外元素,Record 则没有 rest element。
用类型转换模式代替类型检查
value as int 转换失败会抛出异常,int value 匹配失败则可以继续尝试其他分支。需要正常处理类型不符时,应使用带类型变量模式。
忽略逻辑模式的变量约束
逻辑或的每个分支必须绑定同一组变量;逻辑与的两个分支不能绑定同名变量。组合复杂时,先把常量、关系和结构匹配分开理解,再检查变量在哪里创建。
小结
- 常量、关系和带类型变量模式负责检查值;变量模式负责绑定值。
||、&&和括号模式用于组合规则并控制匹配顺序。?让null匹配失败,!在遇到null时抛出异常,as在类型错误时抛出异常。- List、Map、Record 和 Object 模式按各自结构递归解构内部值。
- Map 与 Object 模式允许未列出的内容;List 与 Record 模式要求完整形状,List 可以用
...接收剩余元素。 _可以忽略值,Type _可以只检查类型而不绑定变量。