Dart 3 Pattern 模式

Patterns(模式) 是 Dart 语言中的一种语法类别,就像语句和表达式一样。Patterns 表示它可能与实际值匹配的一组值的形状。

逻辑或

subpattern1 || subpattern2

一个逻辑或模式通过 || 分隔子模式,如果任何一个分支匹配,则匹配。分支从左到右进行执行。一旦一个分支匹配,其余的分支就不会被执行。

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

子模式中的逻辑或模式可以绑定变量,但是分支必须定义相同的变量集,因为只有一个分支在模式匹配时才会被执行。

逻辑与

subpattern1 && subpattern2

一对由 && 分隔的模式仅在两个子模式都匹配时才匹配。如果左分支不匹配,则不会执行右分支。

逻辑与模式中的子模式可以绑定变量,但是每个子模式中的变量不能重叠,因为如果模式匹配,则两个变量都将被绑定:

1
2
3
4
switch ((1, 2)) {
// 错误,两个子模式都尝试绑定 'b'。
case (var a, var b) && (var b, var c): // ...
}

关系

== expression

< expression

关系模式使用任何等式或关系运算符将匹配的值与给定的常量进行比较:==!=<><=>=

当使用常量作为参数调用匹配值上的适当运算符返回 true 时,模式匹配。

关系模式对于匹配数字范围特别有用,特别是与 逻辑与模式 结合使用时:

1
2
3
4
5
6
7
8
9
10
11
12
13
String asciiCharType(int char) {
const space = 32;
const zero = 48;
const nine = 57;

return switch (char) {
< space => 'control',
== space => 'space',
> space && < zero => 'punctuation',
>= zero && <= nine => 'digit',
_ => ''
};
}

强制转换

foo as String

强制转换模式允许您在解构之前,在将值传递给另一个子模式之前,在解构的中间插入 类型转换:

1
2
(num, Object) record = (1, 's');
var (i as int, s as String) = record;

强制转换模式将在值没有指定类型时抛出错误。与 空断言 模式 一样,这允许您强制断言某些解构值的预期类型。

空检查

subpattern?

空检查模式首先匹配,如果值不为 null,然后匹配内部模式与相同的值。它们允许您绑定一个变量,其类型是匹配的可空值的非空基本类型。

要将 null 值视为匹配失败而不抛出,请使用空检查模式。

1
2
3
4
5
String? maybeString = 'nullable with base type String';
switch (maybeString) {
case var s?:
// 's' 在这里具有非空 String 类型。
}

为了在值为 null 时匹配,请使用 常量模式 null

空断言

subpattern!

为了确保 null 值不会被静默地视为匹配失败,请在匹配时使用空断言模式:

1
2
3
4
5
List<String?> row = ['user', null];
switch (row) {
case ['user', var name!]: // ...
// 'name' 是一个非空 String 类型。
}

为了从变量声明模式中消除 null 值,请使用空断言模式:

1
2
3
(int?, int?) position = (2, 3);

var (x!, y!) = position;

为了在值为 null 时匹配,请使用 常量模式 null

常量

123, null, 'string', math.pi, SomeClass.constant, const Thing(1, 2), const (1 + 2)

常量模式在值等于常量时匹配:

1
2
3
4
switch (number) {
// 如果 1 == number,匹配
case 1: // ...
}

你可以直接使用简单的字面量和命名常量引用作为常量模式:

  • 数字字面量 (123, 45.56)
  • 布尔字面量 (true)
  • 字符串字面量 ('string')
  • 命名常量 (someConstant, math.pi, double.infinity)
  • 常量构造函数 (const Point(0, 0))
  • 常量集合字面量 (const [], const {1, 2})

更多的复杂常量表达式必须用括号括起来,并以 const 为前缀 (const (1 + 2)):

1
2
3
4
5
// 列表模式匹配
case [a, b]: // ...

// 列表字面量模式匹配
case const [a, b]: // ...

变量

var bar, String str, final int _

变量模式将新变量绑定到已匹配或解构的值。它们通常作为 解构模式 的一部分出现,以捕获解构值。

这些变量在代码区域中是可见的,该区域仅在模式匹配时才可使用。

1
2
3
4
5
switch ((1, 2)) {
// 变量 'var a' 和 'var b' 是变量模式,分别绑定到 1 和 2。
case (var a, var b): // ...
// 'a' 和 'b' 在 case 体中是可使用的。
}

一个 有类型 变量模式仅在匹配的值具有声明的类型时才匹配,否则失败:

1
2
3
4
switch ((1, 2)) {
// 不匹配
case (int a, String b): // ...
}

Switch 语句可以有多个 case 共享一个语句,而无需使用逻辑或模式,但是它们仍然非常有用,因为它们允许多个 case 共享一个 guard:

1
2
3
4
switch (shape) {
case Square(size: var s) || Circle(size: var s) when s > 0:
print('非空正方形或圆形');
}

Guard 从句 作为 case 的一部分评估任意条件,如果条件为 false,则不会退出 switch(就像在 case 体中使用 if 语句会导致的那样)。

1
2
3
4
5
6
7
8
9
10
switch (pair) {
case (int a, int b):
if (a > b) print('First element greater');
// 如果为 false,则不打印任何内容并退出 switch。
case (int a, int b) when a > b:
// 如果为 false,则不打印任何内容,但继续执行下一个 case。
print('First element greater');
case (int a, int b):
print('First element not greater');
}

你可以使用 [通配符模式][#通配符] 作为变量模式。

标识符

foo, _

标识符模式可能会像 [常量模式][#常量] 或 [变量模式][#变量] 一样,具体取决于它们出现的上下文:

  • 声明上下文:使用标识符名称声明一个新变量:var (a, b) = (1, 2);
  • 赋值上下文:将现有变量与标识符名称分配:(a, b) = (3, 4);
  • 匹配上下文:被视为命名常量模式(除非其名称为 _):
    1
    2
    3
    4
    5
    6
    7
    const c = 1;
    switch (2) {
    case c:
    print('match $c');
    default:
    print('no match'); // 打印 "no match".
    }
  • 通配符标识符在任何上下文中:匹配任何值并将其丢弃:case [_, var y, _]: print('The middle element is $y');

括号

(subpattern)

类似于括号表达式,模式中的括号允许您控制 模式优先级 并在需要较高优先级的模式的位置插入较低优先级的模式。

例如,假设布尔常量 xyz 分别等于 truetruefalse

1
2
3
4
// ...
x || y && z => 'matches true',
(x || y) && z => 'matches false',
// ...

在第一种情况下,逻辑与模式 y && z 首先计算,因为逻辑与模式的优先级高于逻辑或。在下一个 case 中,逻辑或模式被括号括起来。它首先计算,这导致不同的匹配。

列表

[subpattern1, subpattern2]

一个列表模式匹配实现 List 的值,然后递归地将其子模式与列表的元素匹配,以按位置解构它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const a = 'a';
const b = 'b';
switch (obj) {
// 列表模式 [a, b] 首先匹配 obj,如果 obj 是一个具有两个字段的列表,然后如果它的字段与常量子模式 'a' 和 'b' 匹配。
case [a, b]:
print('$a, $b');
}
```

列表模式要求模式中的元素数量与整个列表匹配。但是,您可以使用 [剩余元素](#剩余元素) 作为占位符来解决列表中任意数量的元素。

### 剩余元素

列表模式可以包含 _一个_ 剩余元素 (`...`),它允许匹配任意长度的列表。

```dart
var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
// 打印 "1 2 6 7".
print('$a $b $c $d');

一个剩余元素也可以有一个子模式,该子模式将不匹配列表中的其他子模式的元素收集到一个新列表中:

1
2
3
var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
// 打印 "1 2 [3, 4, 5] 6 7".
print('$a $b $rest $c $d');

Map

{"key": subpattern1, someConst: subpattern2}

一个 map 模式匹配实现 Map 的值,然后递归地将其子模式与 map 的键匹配,以解构它们。

map 模式不要求模式与整个 map 匹配。map 模式忽略 map 包含的任何模式不匹配的键。

for 和 for-in 循环

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

1
2
3
4
5
6
7
8
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。它在每次迭代中调用 MapEntry 上的 key getter 和 value getter,并将结果分别绑定到局部变量 keycount

将 getter 调用的结果绑定到同名变量是一种常见用例,因此对象模式还可以从 [变量子模式][variable] 推断 getter 名称。这允许您将变量模式从冗余的 key: key 简化为 :key

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

Record

(subpattern1, subpattern2)

(x: subpattern1, y: subpattern2)

Record 模式匹配一个 record 对象并解构其字段。如果值不是与模式具有相同 shape 的记录,则匹配失败。否则,字段子模式将与记录中的相应字段匹配。

Record 模式要求模式与整个记录匹配。要使用模式解构具有 命名 字段的记录,请在模式中包含字段名称:

1
var (myString: foo, myNumber: bar) = (myString: 'string', myNumber: 1);

getter 名称可以省略并从字段子模式中的 变量模式标识符模式 推断出来。这些模式对是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 带有变量子模式的记录模式:
var (untyped: untyped, typed: int typed) = record;
var (:untyped, :int typed) = record;

switch (record) {
case (untyped: var untyped, typed: int typed): // ...
case (:var untyped, :int typed): // ...
}

// 带有空检查和空断言子模式的记录模式:
switch (record) {
case (checked: var checked?, asserted: var asserted!): // ...
case (:var checked?, :var asserted!): // ...
}

// 带有强制转换子模式的记录模式:
var (untyped: untyped as int, typed: typed as String) = record;
var (:untyped as int, :typed as String) = record;

对象

SomeClass(x: subpattern1, y: subpattern2)

对象模式检查匹配的值是否与给定的命名类型匹配,以使用对象属性上的 getter 解构数据。如果值没有相同的类型,则它们不匹配。

1
2
3
4
5
6
7
8
9
10
11
switch (shape) {
// 如果 shape 是 Rect 类型,则匹配,然后与 Rect 的属性匹配。
case Rect(width: var w, height: var h): // ...
}
```

getter 的名称可以省略并从字段子模式中的 [变量模式](#变量) 或 [标识符模式](#标识符) 推断出来:

```dart
// 绑定新变量 x 和 y 到 Point 的 x 和 y 属性的值。
var Point(:x, :y) = Point(1, 2);

对象模式不要求模式与整个对象匹配。如果对象具有模式不解构的额外字段,则仍然可以匹配。

通配符

_

一个名为 _ 的模式是一个通配符,可以是 变量模式标识符模式,它不绑定或分配给任何变量。

它在需要子模式以便稍后解构位置值的位置上很有用:

1
2
var list = [1, 2, 3];
var [_, two, _] = list;

带有类型注释的通配符名称在您想要测试值的类型但不想将值绑定到名称时很有用:

1
2
3
4
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}

模式的用例

本节描述了更多用例,回答了以下问题:

  • 何时和为什么 您可能想要使用模式。
  • 它们解决了哪些问题。
  • 它们最适合哪些习惯用法。

解构多个返回

模式添加了将记录字段直接解构到局部变量的能力,与函数调用一起内联。

例如:

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

你还可以将函数返回的记录的字段解构为局部变量,并将记录模式作为其子模式:

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

解构 class 实例

对象模式 与命名对象类型匹配,允许您使用对象的类已经公开的 getter 解构其数据。

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

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

代数数据类型

对象解构 和 switch case 有助于以 代数数据类型 风格编写代码。
在以下情况下使用此方法:

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

不要将操作实现为每种类型的实例方法,而是将操作的变体保留在单个函数中,该函数在子类型上切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

模式非常适合解构 JSON 数据中的键值对:

1
2
3
4
var json = {
'user': ['Lily', 13]
};
var {'user': [name, age]} = json;

如果您知道 JSON 数据具有您期望的结构,则上一个示例是现实的。但是数据通常来自外部来源,例如通过网络。您需要首先验证它以确认其结构。

没有模式,验证很冗长:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (json is Map<String, Object?> &&
json.length == 1 &&
json.containsKey('user')) {
var user = json['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.');
}
}

模式提供了一种更声明性的验证 JSON 的方法,而且不那么冗长:

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

此 case 模式同时验证:

  • json 是一个 map,因为它必须首先匹配外部 map 才能继续。
  • 并且,由于它是一个 map,它还确认 json 不为 null。
  • json 包含一个键 user
  • user 与两个值的列表配对。
  • 列表值的类型是 Stringint
  • 用于保存值的新局部变量是 Stringint