Skip to content

模式(Patterns)
版本说明
模式功能需要 Dart 语言版本至少为 3.0。

模式是 Dart 语言中的一种语法类别,类似于语句和表达式。一个模式表示一组值的可能形状,它可以与实际值进行匹配。

本页介绍:

  • 模式的作用
  • 模式在 Dart 代码中的使用位置
  • 模式的常见使用场景
  • 要了解不同种类的模式,请访问“模式类型”页面。

模式的作用

一般来说,模式可以用于匹配值、解构值,或者两者兼有,具体取决于上下文和模式的形状。

首先,模式匹配允许你检查给定值是否:

  • 具有某种形状
  • 是某个常量
  • 等于其他值
  • 属于某种类型

然后,模式解构提供了一种方便的声明性语法,将值拆分为其组成部分。同一个模式还可以在这个过程中将变量绑定到部分或全部这些组成部分。


匹配

模式总是对一个值进行测试,以确定该值是否符合你期望的形式。换句话说,你是在检查该值是否与模式匹配。

什么构成匹配取决于你使用的模式类型。例如,常量模式在值等于模式的常量时匹配:

dart
switch (number) {
  // 如果 1 == number,则常量模式匹配
  case 1:
    print('one');
}

许多模式会使用子模式(有时称为外部模式和内部模式)。模式会递归地在其子模式上进行匹配。例如,任何集合类型模式的各个字段可以是变量模式或常量模式:

dart
const a = 'a';
const b = 'b';
switch (obj) {
  // 列表模式 [a, b] 首先检查 obj 是否是一个有两个字段的列表,
  // 然后检查其字段是否匹配常量子模式 'a' 和 'b'
  case [a, b]:
    print('$a, $b');
}

若要忽略匹配值中的某些部分,可以使用通配符模式作为占位符。在列表模式中,你可以使用 rest 元素。


解构

当一个对象与模式匹配时,该模式可以访问该对象的数据并将其拆分为部分。换句话说,模式对对象进行了“解构”:

dart
var numList = [1, 2, 3];
// 列表模式 [a, b, c] 从 numList 中解构出三个元素...
var [a, b, c] = numList;
// ...并将它们赋值给新的变量
print(a + b + c);

你可以在解构模式中嵌套任何类型的模式。例如,此 case 模式匹配并解构一个两元素列表,其第一个元素是 'a' 或 'b':

dart
switch (list) {
  case ['a' || 'b', var c]:
    print(c);
}

模式可以出现的位置

你可以在 Dart 语言的多个地方使用模式:

  • 局部变量声明和赋值
  • for 和 for-in 循环
  • if-case 和 switch-case
  • 集合字面量中的控制流

本节介绍使用模式进行匹配和解构的常见用例。


变量声明

你可以在 Dart 允许局部变量声明的任何地方使用模式变量声明。该模式会对声明右侧的值进行匹配。一旦匹配成功,它就会解构该值并将其绑定到新的局部变量:

dart
// 声明新变量 a、b 和 c
var (a, [b, c]) = ('str', [1, 2]);

模式变量声明必须以 varfinal 开头,后跟一个模式。


变量赋值

变量赋值模式位于赋值的左侧。首先,它对匹配的对象进行解构。然后,它将值赋给已存在的变量,而不是绑定新的变量。

使用变量赋值模式可以在不声明第三个临时变量的情况下交换两个变量的值:

dart
var (a, b) = ('left', 'right');
(b, a) = (a, b); // 交换
print('$a $b'); // 输出 "right left"

switch 语句和表达式

每个 case 子句都包含一个模式。这适用于 switch 语句和表达式,也适用于 if-case 语句。你可以在 case 中使用任何类型的模式。

case 模式是可反驳的(refutable)。它们允许控制流:

  • 匹配并解构被切换的对象
  • 如果对象不匹配,则继续执行

case 中模式解构出的值会成为局部变量。它们的作用域仅限于该 case 的主体内。

dart
switch (obj) {
  // 如果 1 == obj,则匹配
  case 1:
    print('one');

  // 如果 obj 的值在 'first' 和 'last' 的常量值之间,则匹配
  case >= first && <= last:
    print('in range');

  // 如果 obj 是一个具有两个字段的记录,
  // 则将字段赋值给 'a' 和 'b'
  case (var a, var b):
    print('a = $a, b = $b');

  default:
}

逻辑或模式(logical-or patterns)对于在 switch 表达式或语句中共享多个 case 的主体非常有用:

dart
var isPrimary = switch (color) {
  Color.red || Color.yellow || Color.blue => true,
  _ => false,
};

switch 语句可以不使用逻辑或模式就让多个 case 共享一个主体,但它们在允许多个 case 共享一个 guard(守卫条件)方面仍然非常有用:

dart
switch (shape) {
  case Square(size: var s) || Circle(size: var s) when s > 0:
    print('Non-empty symmetric shape');
}

guard 子句(guard clauses)在 case 中评估任意条件,而不会在条件为假时退出 switch(不像在 case 主体中使用 if 语句那样会导致退出)。

dart
switch (pair) {
  case (int a, int b):
    if (a > b) print('First element greater');
  // 如果为假,则不输出任何内容并退出 switch
  case (int a, int b) when a > b:
    // 如果为假,则不输出任何内容但继续到下一个 case
    print('First element greater');
  case (int a, int b):
    print('First element not greater');
}

for 和 for-in 循环

你可以在 for 和 for-in 循环中使用模式来遍历并解构集合中的值。

此示例在 for-in 循环中使用对象解构来解构 <Map>.entries 调用返回的 MapEntry 对象:

dart
Map<String, int> hist = {'a': 23, 'b': 100};

for (var MapEntry(key: key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

对象模式检查 hist.entries 是否具有命名类型 MapEntry,然后递归进入命名字段子模式 keyvalue。它在每次迭代中调用 MapEntrykey getter 和 value getter,并将结果分别绑定到局部变量 keycount

将 getter 调用的结果绑定到同名变量是一种常见用例,因此对象模式也可以从变量子模式中推断出 getter 名称。这允许你将冗长的变量模式(如 key: key)简化为更简洁的形式 :key

dart
for (var MapEntry(:key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

模式的使用场景

前一节描述了模式如何融入其他 Dart 代码结构中。你已看到一些有趣的用例示例,例如交换两个变量的值,或者解构 map 中的键值对。本节将介绍更多使用场景,解答以下问题:

  • 何时以及为何可能想要使用模式
  • 它们解决了哪些类型的问题
  • 它们最适合哪些惯用法

解构多个返回值

记录(Records)允许从单个函数调用中聚合并返回多个值。模式增加了直接将记录字段解构为局部变量的能力,与函数调用内联进行。

无需像这样分别为每个记录字段声明新的局部变量:

dart
var info = userInfo(json);
var name = info.$1;
var age = info.$2;

你可以使用变量声明或赋值模式,以及作为其子模式的记录模式,将函数返回的记录字段解构为局部变量:

dart
var (name, age) = userInfo(json);

使用模式解构具有命名字段的记录:

dart
final (:name, :age) = getData(); // 例如,返回 (name: 'doug', age: 25);

解构类实例

对象模式可以匹配命名对象类型,允许你使用该对象类已经暴露的 getter 来解构其数据。

要解构一个类的实例,使用命名类型,后跟括号中要解构的属性:

dart
final Foo myFoo = Foo(one: 'one', two: 2);
var Foo(:one, :two) = myFoo;
print('one $one, two $two');

代数数据类型

对象解构和 switch case 非常适合以代数数据类型(Algebraic Data Type)风格编写代码。当你满足以下条件时,可以使用这种方法:

  • 你有一组相关的类型
  • 你有一个需要对每种类型执行特定行为的操作
  • 你希望将该行为集中在一个地方,而不是分散在所有不同类型定义中

与其为每种类型实现该操作的实例方法,不如将操作的不同变体保留在一个函数中,该函数根据子类型进行切换:

dart
sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
  Square(length: var l) => l * l,
  Circle(radius: var r) => math.pi * r * r,
};

验证传入的 JSON

Map 和列表模式非常适合解构反序列化数据(例如从 JSON 解析的数据)中的键值对:

dart
var data = {
  'user': ['Lily', 13],
};
var {'user': [name, age]} = data;

如果你知道 JSON 数据具有你期望的结构,那么前面的示例是现实的。但数据通常来自外部源,例如通过网络。你需要先验证它以确认其结构。

没有模式的情况下,验证会非常冗长:

dart
if (data is Map<String, Object?> &&
    data.length == 1 &&
    data.containsKey('user')) {
  var user = data['user'];
  if (user is List<Object> &&
      user.length == 2 &&
      user[0] is String &&
      user[1] is int) {
    var name = user[0] as String;
    var age = user[1] as int;
    print('User $name is $age years old.');
  }
}

单个 case 模式可以实现相同的验证。单个 case 最适合作为 if-case 语句使用。模式提供了一种更声明性、且更简洁的方法来验证 JSON:

dart
if (data case {'user': [String name, int age]}) {
  print('User $name is $age years old.');
}

此 case 模式同时验证了:

  • json 是一个 map,因为它必须首先匹配外部 map 模式才能继续
  • 由于它是一个 map,它还确认 json 不为 null
  • json 包含键 user
  • 键 user 对应一个包含两个值的列表
  • 列表值的类型是 String 和 int
  • 新的局部变量用于保存值,分别是 name 和 age