跳到主要内容

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组合规则或改变优先级
检查与绑定关系、常量、变量、通配符>= 18trueString s检查值,并按需绑定变量
后缀检查类型转换、空检查、空断言value as intvalue?在继续匹配前转换或检查子模式的值
结构解构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 namefinal 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-caseswitch 等可失败的匹配中,缺少键会让模式失败;在没有失败分支的声明模式中,读取不存在的键会抛出 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
}

命名字段中的 :namename: 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
}

这个模式依次检查:

  1. 外层值是包含 'user''roles' 键的 Map;
  2. 'user' 是指定形状的 record,idintname 是非空 String
  3. 'roles' 是非空 List,首项为 'admin''editor'
  4. 所有条件成立后,idname 才能在 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 _ 可以只检查类型而不绑定变量。