跳到主要内容

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] 不只要求值是两个元素的列表,还要求两个元素依次为 Stringint

解构值

解构用于从一个复合值中取出组成部分,并直接绑定到变量:

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 patternUser(:name, :age)检查对象类型并调用 getter 解构属性
关系模式>= 0 && <= 100使用关系和逻辑模式组合条件
或模式'yes' || 'y'任一子模式匹配即可
空检查模式var value?只匹配非空值并绑定非空类型变量

本篇重点介绍模式的使用位置和整体思路;不同模式的完整语法可以在掌握这些场景后继续分别学习。

在变量声明中解构

局部变量声明可以在 varfinal 后直接使用模式。右侧的值必须与模式匹配,因为声明没有“匹配失败后执行其他分支”的位置。

({String name, int lessons}) loadCourse() {
return (name: 'Dart', lessons: 12);
}

void main() {
final (:name, :lessons) = loadCourse();

print(name); // Dart
print(lessons); // 12
}

在命名 record pattern 中,:namename: name 的简写:字段名和新变量名相同时,可以省略重复部分。

变量声明模式必须是不可反驳的(irrefutable),也就是对右侧的静态类型必然能够匹配。可能失败的类型检查或常量比较,应放在 if-caseswitch 中处理。

给已有变量赋值

赋值模式不会创建变量,而是解构右侧的值并更新已经存在的变量。交换两个变量时不需要临时变量:

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 的列表,并且元素类型依次为 Stringint。任一步不匹配都会进入 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.keyentry.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-caseswitch 模式一样,在一个结构中递归完成常量匹配、运行时类型检查和变量绑定。处理联合类型时,TypeScript 通常依赖判别字段、ifswitch 来缩小类型。

常见误区

把模式当作普通布尔表达式

模式不是独立返回 bool 的表达式,不能写成 if ([a, b])。需要在条件中匹配时,应使用 if (value case [var a, var b])

在声明中使用可能失败的模式

变量声明必须保证模式能够匹配。外部数据的结构不可靠时,不要直接解构;使用 if-caseswitch 为失败情况提供分支。

认为列表模式会忽略多余元素

[first, second] 只匹配长度为 2 的列表。需要接收其余元素时,应显式使用 rest element,例如 [first, ...rest]

忘记守卫失败后仍会继续匹配

switch 中,某个 case 的模式成功但守卫为 false 时,Dart 会继续尝试后续 case。把条件写在 case 体内的 if 中则不会产生相同的分支选择行为。

小结

  • 模式描述值的形状,可以匹配、解构并绑定变量。
  • 变量声明和赋值模式用于已知结构;可能失败的匹配应使用 if-caseswitch
  • 模式可以递归嵌套,适合处理 record、List、Map 和对象。
  • when 守卫用于补充模式无法表达的业务条件。
  • _ 可以忽略不关心的部分,list pattern 需要用 rest element 明确接收剩余元素。
  • 模式与 sealed class 搭配,可以让分析器检查 switch 是否穷尽。