
前言
收到一张采购单,我们会先确认它写了商品和数量,再决定如何处理。如果只有"苹果",没有写要几个,采购员就没法照单买齐。确认信息是否完整,与取出其中的商品、数量,是两件相连的事。
程序处理数据也常做这两件事。拿到一条记录,我们想把几个字段分别取出来;拿到外部传来的数据,还得先确认它的结构和类型是否符合预期。Dart 的模式把"按什么结构检查"和"从中取出什么"写在一起。
一、模式到底是什么
**模式是用来描述值应有形状的一种写法。**它可以检查一个值是否符合这种形状;匹配成功后,还可以把其中的值取出来,放进变量。模式不是新的数据类型,也不会把原来的值改成另一种数据。Dart 3.0 起支持模式。
先看一条记录:
dart
void main() {
final purchase = (item: '苹果', count: 3);
final (item: itemName, count: quantity) = purchase;
print(itemName); // 苹果
print(quantity); // 3
}
等号右侧是已有的记录,它有 item 和 count 两个字段。等号左侧的 (item: itemName, count: quantity) 是模式:冒号前是要找的字段名,冒号后是接住字段值的新变量名。因此 itemName 得到"苹果",quantity 得到 3。这里已知 purchase 是符合要求的记录,所以可以直接拆开。
模式有两个动作:匹配 是核对结构,解构 是取出结构中的值。有时只需要其中一个动作,例如检查温度是否小于零;有时两者一起使用,例如确认一份数据包含姓名,再取出姓名。Dart 模式
二、已经确定结构时,直接拆值
2.1 声明新变量并拆值
计算两次成绩后,函数可以把总分和平均分放进一条记录返回。模式则能把这两个结果一次取出:
dart
({int total, double average}) summarize(int first, int second) {
final total = first + second;
return (total: total, average: total / 2);
}
void main() {
final (:total, :average) = summarize(80, 100);
print('$total, $average'); // 180, 90.0
}
summarize() 负责计算成绩并返回记录;模式不参与计算,只负责取出结果。(:total, :average) 是简写,意思是从同名的 total、average 字段取值,并声明同名变量。它相当于先保存返回的记录,再分别读取 result.total 和 result.average。
列表也能拆,但位置要对齐:
dart
void main() {
final [first, second] = ['苹果', '香蕉'];
print('$first、$second'); // 苹果、香蕉
}
这里要求右侧恰好有两个元素。如果元素个数不确定,可以用 ... 表示其余部分:
dart
void main() {
final [first, ...rest] = ['苹果', '香蕉', '梨'];
print(first); // 苹果
print(rest); // [香蕉, 梨]
}
rest 得到剩余元素组成的列表;如果只想略过剩余元素,可写 ...。当数据可能不符合形状时,不要直接拆值,因为这种声明要求模式一定能匹配右侧的值。列表模式与剩余元素
模式也能放在 for 循环中。每次循环取出一条记录时,(name, count) 会把两个位置字段分别绑定到变量:
dart
void main() {
final items = [('苹果', 2), ('梨', 3)];
for (final (name, count) in items) {
print('$name:$count');
}
}
循环不是先取出整条记录,再手写 item. 1和item.2;模式在每次迭代时完成拆值。列表中的某一项若不符合 (String, int),循环也不能把它当成符合该形状的记录。
2.2 给已有变量赋值
模式既能声明新变量,也能给已有变量赋值。例如交换两个值:
dart
void main() {
var left = '左';
var right = '右';
(left, right) = (right, left);
print('$left、$right'); // 右、左
}
等号左侧是赋值模式,left、right 已经存在;它没有重新声明变量。等号右侧先组成记录,再分别赋给左侧。final (...) = ... 会创建新变量,这里的 (left, right) = ... 则改变已有变量的值。
三、结构不确定时,先匹配再使用
前面的 (item: '苹果', count: 3) 是记录 ,有确定的字段。接口或文件传来的采购单也可能是 {'item': '苹果', 'count': 3} 这样的 Map,用键查找对应的值。两者都能表达"苹果,3 个",但它们是不同的数据类型。下面用手写的 Map 模拟尚未核对的外部数据:它可能缺少 count,也可能把数量写成字符串。由于函数接收的是 Object?,使用前要先确认传入的确实是包含所需键、且值类型正确的 Map。
3.1 映射模式:同时检查键和值
dart
void printPurchase(Object? data) {
if (data case {'item': String item, 'count': int count}) {
print('$item:$count 个');
} else {
print('采购单信息不完整');
}
}
void main() {
printPurchase({'item': '苹果', 'count': 3});
printPurchase({'item': '苹果'});
}
第一次传入时,模式找到 item 和 count 两个键,值分别是 String 和 int,因此打印"苹果:3 个"。第二次缺少 count,匹配失败,进入 else。即使键存在,若数量的值是字符串 '3',也不会匹配 int count。这里没有把字符串自动转换为整数。
读 if (data case ...) 时,先看 data:它是待检查的值;再看 case 后面的大括号:它描述需要的键和各自的类型。检查成功才进入第一个分支,此时 item、count 已是可用的局部变量;失败就进入 else,不能在那儿读取这两个变量。这就是映射模式 。它只检查写出来的键,数据中再多一个其他键仍可匹配;列表模式通常要求元素个数与模式对应,而映射模式不要求列出所有键。映射模式
3.2 守卫条件:匹配后继续检查
如果还想检查"购买数量必须大于零",可以在模式之后加 when:
dart
void printCount(Object? data) {
if (data case {'count': int count} when count > 0) {
print('数量:$count');
} else {
print('数量无效');
}
}
先确认 count 是整数,才能安全地比较它是否大于零。when 是守卫条件 :模式匹配成功后再检查条件;条件为假,同样走 else。它不负责从缺失的数据里补出值。Dart 守卫条件

图中的第一关核对数据结构和类型,第二关才判断数量是否大于零。只有两关都通过,程序才会执行成功分支。因此"有 count 键,但值是 '3'"与"值是整数 0"都会进入 else,只是失败的关口不同。
3.3 空值检查:只让非空值进入分支
模式还可以顺手排除 null。问号放在变量模式后面,表示"匹配非空值,并把非空值绑定出来":
dart
void printNickname(String? nickname) {
if (nickname case var value?) {
print(value.length);
} else {
print('没有昵称');
}
}
value 在成功分支里已经是非空的 String,所以可以直接读取 length。如果 nickname 是 null,模式失败,进入 else。这和先写 if (nickname != null) 再读取也能达到同样目的;模式的优势是检查和绑定写在一起。
四、模式不只拆记录和映射
模式可以依照不同形状取值。除了记录、列表和映射,也能检查一个对象的类型,并读取它的属性:
dart
class Book {
final String title;
final int pages;
Book(this.title, this.pages);
}
void main() {
Object item = Book('小王子', 96);
if (item case Book(title: var title)) {
print(title); // 小王子
}
}
Book(...) 先要求值是 Book,再读取它的 title getter,并把结果放进局部变量 title。虽然类还有 pages,这里不需要取,就不必写。对象模式不会调用构造函数创建新书 ,它是在查看已有对象。对象模式
如果某个位置的值不需要用,可以用 _ 略过:
dart
void main() {
final (_, count) = ('苹果', 3);
print(count); // 3
}
_ 是通配模式:这个位置要有值,但我们不把它绑定到变量。它不是给字段起了一个可读取的名字。上面的记录仍要有两个位置字段,只是我们只关心第二个。模式也能逐层嵌套:如果记录的某个字段本身是列表,可以在那个字段的位置再写列表模式,继续取出其中的元素。
五、多个可能结果,用 switch 分别处理
匹配不一定要拆出变量。有时我们只想根据值作判断。例如根据温度给出提示:
dart
String describeTemperature(int temperature) => switch (temperature) {
< 0 => '结冰',
>= 30 => '炎热',
_ => '适中',
};
void main() {
print(describeTemperature(-2)); // 结冰
print(describeTemperature(20)); // 适中
}
< 0 和 >= 30 是关系模式,按顺序尝试。_ 接住没有匹配的值。这里用的是 switch 表达式,它要给函数产生一个结果,所以所有可能的输入都得有对应分支;漏掉兜底分支可能无法通过穷尽性检查。
如果要按某个确定的值判断,直接把该值写在分支里。例如状态是 'pending' 或 'waiting' 时都提示等待,可以用 || 把两个常量模式合在一支:
dart
String message(String status) => switch (status) {
'pending' || 'waiting' => '请等待',
'sent' => '已发货',
_ => '状态未知',
};
这里的 || 表示任一模式匹配即可,不是在比较两个字符串本身;_ 处理未列出的状态。这样读分支时,就能清楚看到哪些输入共享同一个结果。逻辑或模式
switch 也能结合拆值。例如处理记录时,先按字段匹配,再给出结果:
dart
String checkPurchase(({String item, int count}) purchase) =>
switch (purchase) {
(item: _, count: <= 0) => '数量无效',
(item: var item, count: var count) => '$item:$count 个',
};
记录模式仍要写出全部字段,所以第一个分支用 _ 略过商品名,只判断数量是否有效;第二个分支取出商品名和数量。由于参数已限定为有 item 和 count 的记录,第二个分支可以覆盖其余情况。这里的参数类型已经由调用方保证;如果拿到的是未经核对的外部数据,就应先匹配结构和类型。Dart 分支与模式
六、什么时候使用模式
拿到结构已知的记录或列表,只想把几部分分别使用时,直接用模式解构。拿到不确定的映射或对象,必须先核对结构和类型时,用 if-case 或 switch 尝试匹配。只用一个字段时,普通的 purchase.item 往往更清楚;模式不是每次取值都必须使用的新语法。
读模式时,可以从外向内看:**先看值整体是什么形状,再看每个位置要求什么,最后看哪些值被绑定成变量。**如果还带 when,那是在前面的结构匹配成功后继续检查条件。按这个顺序,嵌套模式也不会变成一团符号。
七、总结
模式把"值应有什么结构"写出来,既能检查,也能拆出需要的部分。直接声明或赋值适合形状确定的数据;if-case 和 switch 适合可能匹配失败的分支。列表模式默认对齐元素个数,映射模式只检查指定的键,对象模式检查类型并读取指定属性。
先判断数据是否可靠,再决定直接拆值还是尝试匹配。需要的只是一次普通属性访问时,就用普通访问;需要同时核对结构和取值时,模式才真正省去重复检查。
八、测试题
- final first, second = '苹果', '香蕉', '梨'; 为什么不能按前两个元素直接解构?怎样保留其余元素?
- if (data case {'name': String name}) 遇到 {'name': '小林', 'age': 18} 会匹配吗?如果 name 是整数呢?
- final (_, count) = ('苹果', 3); 中 _ 之后还能作为变量读取吗?
- if (data case {'count': int count} when count > 0) 中,检查"整数"和检查"大于零"的先后顺序是什么?
- 什么时候适合写 book.title,什么时候适合用 Book(title: var title) 模式?