深色模式
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');
}
若要忽略匹配值的某些部分,你可以使用通配符模式作为占位符。对于列表模式,你可以使用剩余元素。
解构
当一个对象与模式匹配时,模式就可以访问该对象的数据并将其拆分为多个部分。换句话说,模式对对象进行了解构:
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]);
模式变量声明必须以 var
或 final
开头,后面跟着一个模式。
变量赋值
变量赋值模式位于赋值语句的左侧。首先,它会解构匹配的对象。然后,它将值赋给现有的变量,而不是绑定新的变量。
使用变量赋值模式可以在不声明第三个临时变量的情况下交换两个变量的值:
dart
var (a, b) = ('left', 'right');
(b, a) = (a, b); // 交换。
print('$a $b'); // 输出 "right left"。
switch
语句和表达式
每个 case
子句都包含一个模式。这适用于 switch
语句和表达式,以及 if-case
语句。你可以在 case
中使用任何类型的模式。
case
模式是可反驳的。它们允许控制流:
- 匹配并解构正在进行
switch
操作的对象。 - 如果对象不匹配,则继续执行。
模式在 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:
}
逻辑或模式在 switch
表达式或语句中让多个 case
共享一个主体时很有用:
dart
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false,
};
switch
语句可以让多个 case
共享一个主体而不使用逻辑或模式,但逻辑或模式在让多个 case
共享一个守卫条件时仍然有独特的用处:
dart
switch (shape) {
case Square(size: var s) || Circle(size: var s) when s > 0:
print('Non-empty symmetric shape');
}
守卫子句作为 case
的一部分计算一个任意条件,如果条件为 false
,不会退出 switch
(就像在 case
主体中使用 if
语句会导致的那样)。
dart
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');
}
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
,然后递归到命名字段子模式 key
和 value
。在每次迭代中,它会调用 MapEntry
上的 key
getter 和 value
getter,并将结果分别绑定到局部变量 key
和 count
。
将 getter 调用的结果绑定到同名变量是一种常见的用例,因此对象模式也可以从变量子模式推断 getter 名称。这允许你将像 key: key
这样冗余的变量模式简化为 :key
:
dart
for (var MapEntry(:key, value: count) in hist.entries) {
print('$key occurred $count times');
}
模式的用例
上一节介绍了模式如何融入其他 Dart 代码结构。你看到了一些有趣的用例示例,比如交换两个变量的值,或者解构映射中的键值对。本节将介绍更多用例,回答以下问题:
- 你何时以及为何可能想要使用模式。
- 它们解决了哪些类型的问题。
- 它们最适合哪些惯用用法。
解构多个返回值
记录允许从单个函数调用中聚合和返回多个值。模式增加了将记录的字段直接解构为局部变量的能力,与函数调用内联。
你可以不针对每个记录字段单独声明新的局部变量,例如:
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
用例有助于以代数数据类型的风格编写代码。当满足以下条件时,可以使用此方法:
- 你有一组相关的类型。
- 你有一个操作,需要为每种类型提供特定的行为。
- 你想将该行为集中在一个地方,而不是分散在所有不同的类型定义中。
不要为每种类型将操作实现为实例方法,而是将操作的变体放在一个对 subtypes 进行 switch
的单个函数中:
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
映射和列表模式非常适合解构反序列化数据(如从 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
是一个映射,因为它必须首先匹配外部映射模式才能继续。- 而且,由于它是一个映射,它还确认
json
不为null
。 json
包含键user
。- 键
user
对应一个包含两个值的列表。 - 列表值的类型是
String
和int
。 - 用于保存值的新局部变量是
name
和age
。
模式类型
此页面是各类模式的参考文档。若要全面了解模式的工作原理、在 Dart 中可以使用模式的位置以及常见用例,请访问主模式页面。
模式优先级
与运算符优先级类似,模式求值遵循优先级规则。你可以使用带括号的模式来优先计算优先级较低的模式。
本文档按优先级升序列出了模式类型:
逻辑或模式的优先级低于逻辑与模式,逻辑与模式的优先级低于关系模式,依此类推。
后缀一元模式(类型转换、空值检查和空值断言)具有相同的优先级。
其余的基本模式具有最高优先级。集合类型(记录、列表和映射)和对象模式包含其他数据,因此会先作为外部模式进行求值。
逻辑或
plaintext
subpattern1 || subpattern2
逻辑或模式使用 ||
分隔子模式,只要其中任何一个分支匹配,整个模式就匹配。分支从左到右进行求值。一旦某个分支匹配成功,其余分支将不再求值。
dart
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false,
};
逻辑或模式中的子模式可以绑定变量,但各个分支必须定义相同的变量集,因为模式匹配时只会对一个分支进行求值。
逻辑与
plaintext
subpattern1 && subpattern2
由 &&
分隔的一对模式只有在两个子模式都匹配时才会匹配。如果左分支不匹配,右分支将不会被求值。
逻辑与模式中的子模式可以绑定变量,但每个子模式中的变量不能重叠,因为如果模式匹配成功,两个子模式中的变量都会被绑定:
dart
switch ((1, 2)) {
// 错误,两个子模式都尝试绑定 'b'。
case (var a, var b) && (var b, var c): // ...
}
关系
plaintext
== expression
< expression
关系模式使用任何相等或关系运算符(==
、!=
、<
、>
、<=
和 >=
)将匹配的值与给定常量进行比较。
当对匹配的值调用相应的运算符并将常量作为参数传入,返回结果为 true
时,该模式匹配成功。
关系模式在匹配数值范围时非常有用,特别是与逻辑与模式结合使用时:
dart
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',
_ => '',
};
}
类型转换
plaintext
foo as String
类型转换模式允许你在解构过程中插入类型转换,然后再将值传递给另一个子模式:
dart
(num, Object) record = (1, 's');
var (i as int, s as String) = record;
如果值的类型与指定的类型不匹配,类型转换模式会抛出异常。与空值断言模式类似,这允许你强制断言某些解构值的预期类型。
空值检查
plaintext
subpattern?
空值检查模式首先检查值是否不为 null
,如果不为 null
,则将该值与内部模式进行匹配。它们允许你绑定一个变量,该变量的类型是被匹配的可空值的非可空基础类型。
若要在不抛出异常的情况下将 null
值视为匹配失败,请使用空值检查模式。
dart
String? maybeString = 'nullable with base type String';
switch (maybeString) {
case var s?:
// 这里的 's' 类型为非可空的 String。
}
若要匹配值为 null
的情况,请使用常量模式 null
。
空值断言
plaintext
subpattern!
空值断言模式首先检查对象是否不为 null
,如果不为 null
,则对该值进行匹配。它们允许非空值通过,但如果匹配的值为 null
,则会抛出异常。
为确保 null
值不会被默默地视为匹配失败,请在匹配时使用空值断言模式:
dart
List<String?> row = ['user', null];
switch (row) {
case ['user', var name!]: // ...
// 这里的 'name' 是一个非可空字符串。
}
若要在变量声明模式中排除 null
值,请使用空值断言模式:
dart
(int?, int?) position = (2, 3);
var (x!, y!) = position;
若要匹配值为 null
的情况,请使用常量模式 null
。
常量
plaintext
123, null, 'string', math.pi, SomeClass.constant, const Thing(1, 2), const (1 + 2)
常量模式在值等于常量时匹配:
dart
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)
):
dart
// 列表或映射模式:
case [a, b]: // ...
// 列表或映射字面量:
case const [a, b]: // ...
变量
plaintext
var bar, String str, final int _
变量模式将新变量绑定到已匹配或解构的值。它们通常作为解构模式的一部分出现,用于捕获解构后的值。
这些变量的作用域是在模式匹配成功后才能访问的代码区域。
dart
switch ((1, 2)) {
// 'var a' 和 'var b' 是变量模式,分别绑定到 1 和 2。
case (var a, var b): // ...
// 'a' 和 'b' 在 case 语句体中处于作用域内。
}
带类型的变量模式只有在匹配的值具有声明的类型时才会匹配,否则匹配失败:
dart
switch ((1, 2)) {
// 不匹配。
case (int a, String b): // ...
}
你可以使用通配符模式作为变量模式。
标识符
plaintext
foo, _
标识符模式的行为可能类似于常量模式,也可能类似于变量模式,具体取决于它们出现的上下文:
- 声明上下文:使用标识符名称声明一个新变量:
var (a, b) = (1, 2);
- 赋值上下文:将值赋给具有该标识符名称的现有变量:
(a, b) = (3, 4);
- 匹配上下文:被视为命名常量模式(除非其名称为
_
):
dart
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');
带括号的模式
plaintext
(subpattern)
与带括号的表达式类似,模式中的括号允许你控制模式的优先级,并在需要高优先级模式的地方插入低优先级模式。
例如,假设布尔常量 x
、y
和 z
分别等于 true
、true
和 false
。尽管以下示例类似于布尔表达式求值,但实际上是在匹配模式。
dart
// ...
x || y => 'matches true',
x || y && z => 'matches true',
x || (y && z) => 'matches true',
// `x || y && z` 与 `x || (y && z)` 是等效的。
(x || y) && z => 'matches nothing',
// ...
Dart 从左到右开始匹配模式。
- 第一个模式匹配
true
,因为x
匹配true
。 - 第二个模式匹配
true
,因为x
匹配true
。 - 第三个模式匹配
true
,因为x
匹配true
。 - 第四个模式
(x || y) && z
不匹配。x
匹配true
,因此 Dart 不会尝试匹配y
。- 尽管
(x || y)
匹配true
,但z
不匹配true
。 - 因此,模式
(x || y) && z
不匹配true
。 - 子模式
(x || y)
不匹配false
,因此 Dart 不会尝试匹配z
。 - 因此,模式
(x || y) && z
不匹配false
。 - 综上所述,
(x || y) && z
不匹配任何值。
列表
plaintext
[subpattern1, subpattern2]
列表模式匹配实现了 List
的值,然后递归地将其子模式与列表的元素进行匹配,以按位置解构这些元素:
dart
const a = 'a';
const b = 'b';
switch (obj) {
// 列表模式 [a, b] 首先检查 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');
剩余元素也可以有一个子模式,该子模式将列表中与其他子模式不匹配的元素收集到一个新列表中:
dart
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');
映射
plaintext
{"key": subpattern1, someConst: subpattern2}
映射模式匹配实现了 Map
的值,然后递归地将其子模式与映射的键进行匹配,以解构这些值。
映射模式不要求模式与整个映射匹配。映射模式会忽略映射中包含的任何未被模式匹配的键。尝试匹配映射中不存在的键会抛出 StateError
:
dart
final {'foo': int? foo} = {};
记录
plaintext
(subpattern1, subpattern2)
(x: subpattern1, y: subpattern2)
记录模式匹配记录对象并解构其字段。如果值不是与模式具有相同结构的记录,则匹配失败。否则,将字段子模式与记录中的相应字段进行匹配。
记录模式要求模式与整个记录匹配。若要使用模式解构具有命名字段的记录,请在模式中包含字段名称:
dart
var (myString: foo, myNumber: bar) = (myString: 'string', myNumber: 1);
可以省略 getter 名称,并从字段子模式中的变量模式或标识符模式推断得出。以下每组模式都是等效的:
dart
// 包含变量子模式的记录模式:
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;
对象
plaintext
SomeClass(x: subpattern1, y: subpattern2)
对象模式将匹配的值与给定的命名类型进行检查,以使用对象属性的 getter 解构数据。如果值的类型不同,则匹配失败。
dart
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);
对象模式不要求模式与整个对象匹配。如果对象有模式未解构的额外字段,仍然可以匹配。
通配符
plaintext
_
名称为 _
的模式是通配符,它可以是变量模式或标识符模式,不会绑定或赋值给任何变量。
在需要子模式来解构后续位置值的地方,它作为占位符很有用:
dart
var list = [1, 2, 3];
var [_, two, _] = list;
带有类型注解的通配符名称在你想测试值的类型但又不想将值绑定到名称时很有用:
dart
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}