Dart 之模式

前言

收到一张采购单,我们会先确认它写了商品和数量,再决定如何处理。如果只有"苹果",没有写要几个,采购员就没法照单买齐。确认信息是否完整,与取出其中的商品、数量,是两件相连的事。

程序处理数据也常做这两件事。拿到一条记录,我们想把几个字段分别取出来;拿到外部传来的数据,还得先确认它的结构和类型是否符合预期。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.1 和 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 适合可能匹配失败的分支。列表模式默认对齐元素个数,映射模式只检查指定的键,对象模式检查类型并读取指定属性。

先判断数据是否可靠,再决定直接拆值还是尝试匹配。需要的只是一次普通属性访问时,就用普通访问;需要同时核对结构和取值时,模式才真正省去重复检查。

八、测试题

  1. final first, second = '苹果', '香蕉', '梨'; 为什么不能按前两个元素直接解构?怎样保留其余元素?
  2. if (data case {'name': String name}) 遇到 {'name': '小林', 'age': 18} 会匹配吗?如果 name 是整数呢?
  3. final (_, count) = ('苹果', 3); 中 _ 之后还能作为变量读取吗?
  4. if (data case {'count': int count} when count > 0) 中,检查"整数"和检查"大于零"的先后顺序是什么?
  5. 什么时候适合写 book.title,什么时候适合用 Book(title: var title) 模式?
相关推荐
淡淡的香烟3 小时前
Flutter实现多语言切换
android·flutter·1024程序员节
AIsoft_86895 小时前
手机视频转文字工具清单:哪些APP支持录屏识别
android·音视频
_祝你今天愉快7 小时前
Paint Cavans高级绘制:[一]Paint画笔高级应用
android
事圆则缓8 小时前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
@zulnger8 小时前
安卓进行稳定性测试
android·appium·1024程序员节
不吃凉粉9 小时前
android 副屏连接
android
OxYGC10 小时前
手机为什么能“自动跳过“开屏广告?李跳跳、GKD 与 iOS 代理软件原理盘点
android·ios·开屏广告·广告屏蔽
Android-Flutter18 小时前
Compose 管理状态 详解
android·compose
缓慢更新19 小时前
审批的加签与会签:多人意见怎么合
android·java·数据库·低代码·rxjava