模式语法
在本节中,我们收集了所有在模式中有效的语法,并讨论了为什么以及何时可能需要使用每一种。
匹配字面量
正如你在第 6 章中看到的,你可以直接将模式与字面量匹配。以下代码给出了一些示例:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
这段代码打印one
,因为x
中的值是1。当您希望代码在获得特定的具体值时采取行动时,这种语法非常有用。
匹配命名变量
命名变量是不可反驳的模式,可以匹配任何值,我们在书中已经多次使用了它们。然而,当你在 match
表达式中使用命名变量时,会有一个复杂的问题。因为 match
开始了一个新的作用域,作为 match
表达式模式一部分声明的变量会遮蔽外部同名的变量,这与所有变量的情况一样。在清单 19-11 中,我们声明了一个名为 x
的变量,其值为 Some(5)
,以及一个值为 10
的变量 y
。然后我们对值 x
创建了一个 match
表达式。查看匹配臂中的模式和最后的 println!
,并尝试在运行此代码或继续阅读之前,先想一想代码将打印什么。
让我们逐步了解当 match
表达式运行时发生了什么。第一个匹配臂中的模式与定义的 x
值不匹配,因此代码继续执行。
第二个匹配臂中的模式引入了一个名为 y
的新变量,该变量将匹配 Some
值中的任何值。因为我们处于 match
表达式内的新作用域中,这是一个新的 y
变量,而不是我们在开头声明的值为 10 的 y
。这个新的 y
绑定将匹配 Some
中的任何值,这正是我们在 x
中所拥有的。因此,这个新的 y
绑定到 x
中 Some
的内部值。该值为 5
,所以该臂的表达式执行并打印 Matched, y = 5
。
如果 x
是一个 None
值而不是 Some(5)
,前两个分支的模式将不会匹配,因此值将匹配到下划线。我们在下划线分支的模式中没有引入 x
变量,因此表达式中的 x
仍然是未被遮蔽的外部 x
。在这种假设情况下,match
将会打印 Default case, x = None
。
当 match
表达式结束时,其作用域结束,内部的 y
的作用域也随之结束。最后一个 println!
产生 at the end: x = Some(5), y = 10
。
要创建一个比较外部 x
和 y
值的 match
表达式,而不是引入一个被遮蔽的变量,我们需要使用匹配卫条件。我们将在 “使用匹配卫条件的额外条件” 部分讨论匹配卫条件。
多个模式
在 match
表达式中,您可以使用 |
语法匹配多个模式,这是模式 或 操作符。例如,在以下代码中,我们匹配 x
的值与匹配臂,其中第一个匹配臂有一个 或 选项,这意味着如果 x
的值与该臂中的任何一个值匹配,该臂的代码将运行:
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
这段代码打印one or two
。
使用 ..=
匹配值的范围
..=
语法允许我们匹配一个包含范围内的值。在以下代码中,当模式匹配给定范围内的任何值时,该分支将执行:
fn main() { let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } }
如果 x
是 1、2、3、4 或 5,第一个分支将匹配。这种语法对于多个匹配值来说比使用 |
运算符表达相同的概念更方便;如果我们使用 |
,则必须指定 1 | 2 | 3 | 4 | 5
。指定一个范围要短得多,特别是如果我们想匹配,比如说,1 到 1,000 之间的任何数字!
编译器在编译时检查范围是否为空,因为只有对于 char
和数值类型,Rust 才能判断一个范围是否为空,所以范围仅允许使用数值或 char
值。
这里是一个使用 char
值范围的例子:
fn main() { let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } }
Rust 可以判断出 'c'
在第一个模式的范围内,并打印 early ASCII letter
。
解构以拆分值
我们也可以使用模式来解构结构体、枚举和元组,以使用这些值的不同部分。让我们逐一了解每个值。
Destructuring Structs
列表 19-12 显示了一个带有两个字段 x
和 y
的 Point
结构体,我们可以使用带有 let
语句的模式将其拆分。
这段代码创建了与 p
结构体的 x
和 y
字段值匹配的变量 a
和 b
。这个例子表明,模式中的变量名称不必与结构体的字段名称匹配。然而,为了更容易记住哪些变量来自哪些字段,通常会将变量名称与字段名称匹配。由于这种常见的用法,以及编写 let Point { x: x, y: y } = p;
包含了大量的重复,Rust 为匹配结构体字段的模式提供了一种简写:你只需要列出结构体字段的名称,从模式创建的变量将具有相同的名称。列表 19-13 的行为与列表 19-12 中的代码相同,但在 let
模式中创建的变量是 x
和 y
而不是 a
和 b
。
这段代码创建了与 p
变量的 x
和 y
字段匹配的变量 x
和 y
。结果是变量 x
和 y
包含了 p
结构体中的值。
我们也可以在结构体模式中使用字面值来解构,而不是为所有字段创建变量。这样做允许我们测试某些字段的特定值,同时为其他字段创建变量来解构。
在清单 19-14 中,我们有一个 match
表达式,它将 Point
值分为三种情况:直接位于 x
轴上的点(当 y = 0
时为真),位于 y
轴上的点(x = 0
),或两者都不是。
第一个分支将通过指定 y
字段的值匹配字面值 0
来匹配任何位于 x
轴上的点。该模式仍然创建一个我们可以在该分支的代码中使用的 x
变量。
同样,第二个分支通过指定 x
字段的值为 0
来匹配 y
轴上的任何点,并为 y
字段的值创建一个变量 y
。第三个分支没有指定任何字面量,因此它匹配任何其他 Point
,并为 x
和 y
字段创建变量。
在这个例子中,值 p
由于 x
包含 0 而匹配第二个分支,因此这段代码将打印 On the y axis at 7
。
记住,match
表达式在找到第一个匹配的模式后就会停止检查其他分支,因此即使 Point { x: 0, y: 0}
位于 x
轴和 y
轴上,这段代码也只会打印 On the x axis at 0
。
Destructuring Enums
我们在本书中已经解构了枚举(例如,第 6 章的示例 6-5),但尚未明确讨论解构枚举的模式对应于枚举中存储的数据的定义方式。例如,在示例 19-15 中,我们使用了来自示例 6-2 的 Message
枚举,并编写了一个 match
,其中的模式将解构每个内部值。
这段代码将打印 将颜色更改为红色 0,绿色 160,和蓝色 255
。尝试更改 msg
的值以查看其他分支的代码运行。
对于没有数据的枚举变体,如Message::Quit
,我们无法进一步解构该值。我们只能匹配字面值Message::Quit
,并且该模式中没有变量。
对于类似结构体的枚举变体,如Message::Move
,我们可以使用与匹配结构体时指定的模式相似的模式。在变体名称之后,我们放置大括号,然后列出带有变量的字段,以便将各个部分拆分出来,在此臂的代码中使用。在这里,我们使用了简写形式,就像我们在清单19-13中所做的那样。
对于像 Message::Write
这样持有一个元素的元组的枚举变体,以及像 Message::ChangeColor
这样持有三个元素的元组的枚举变体,模式与我们指定用于匹配元组的模式相似。模式中的变量数量必须与我们要匹配的变体中的元素数量相匹配。
Destructuring Nested Structs and Enums
到目前为止,我们的示例都只匹配了一层深度的结构体或枚举,但匹配也可以作用于嵌套项!例如,我们可以重构列表 19-15 中的代码,以支持 ChangeColor
消息中的 RGB 和 HSV 颜色,如列表 19-16 所示。
match
表达式的第一臂的模式匹配包含 Color::Rgb
变体的 Message::ChangeColor
枚举变体;然后模式绑定到三个内部的 i32
值。第二臂的模式也匹配 Message::ChangeColor
枚举变体,但内部枚举匹配的是 Color::Hsv
。即使涉及两个枚举,我们也可以在一个 match
表达式中指定这些复杂的条件。
Destructuring Structs and Tuples
我们可以更复杂地混合、匹配和嵌套解构模式。
以下示例展示了一个复杂的解构,其中我们在元组中嵌套了结构体和元组,并解构出所有的原始值:
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
这段代码让我们可以将复杂类型分解为其组成部分,这样我们就可以单独使用我们感兴趣的值。
使用模式解构是一种方便地单独使用值的各个部分的方法,例如从结构体的每个字段中分别获取值。
在模式中忽略值
你已经看到,在模式中有时忽略值是有用的,例如在 match
的最后一个分支中,得到一个实际上不执行任何操作但确实考虑了所有剩余可能值的通配。有几种方法可以在模式中忽略整个值或值的部分:使用 _
模式(你已经见过),在另一个模式中使用 _
模式,使用以下划线开头的名称,或使用 ..
忽略值的剩余部分。让我们探讨如何以及为什么使用每种模式。
Ignoring an Entire Value with _
我们使用下划线作为通配符模式,它可以匹配任何值但不会绑定到该值。这在 match
表达式的最后一个分支中特别有用,但我们也可以在任何模式中使用它,包括函数参数,如列表 19-17 所示。
这段代码将完全忽略作为第一个参数传递的值3
,并将打印This code only uses the y parameter: 4
。
在大多数情况下,当你不再需要某个特定的函数参数时,你会更改签名以不包括这个未使用的参数。忽略函数参数在某些情况下特别有用,例如,当你实现一个特质时,需要特定的类型签名,但你的实现中的函数体不需要其中一个参数。这样,你可以避免因使用名称而产生的未使用函数参数的编译器警告。
Ignoring Parts of a Value with a Nested _
我们也可以在另一个模式中使用 _
来忽略值的一部分,例如,当我们只想测试值的一部分但在相应的代码中不需要其他部分时。列表 19-18 显示了负责管理设置值的代码。业务需求是用户不应被允许覆盖现有设置的自定义,但可以在设置当前未设置时取消设置并为其赋值。
这段代码将打印 Can't overwrite an existing customized value
然后
setting is Some(5)
。在第一个 match 分支中,我们不需要匹配或使用
Some
变体中的任何值,但我们需要测试 setting_value
和 new_setting_value
是 Some
变体的情况。在这种情况下,我们打印不更改 setting_value
的原因,且它不会被更改。
在所有其他情况下(如果 setting_value
或 new_setting_value
为 None
),在第二个分支中用 _
模式表示,我们希望允许 new_setting_value
变为 setting_value
。
我们也可以在同一个模式中的多个地方使用下划线来忽略特定的值。列表 19-19 显示了一个例子,忽略了五个元素元组中的第二个和第四个值。
这段代码将打印 Some numbers: 2, 8, 32
,并且值 4 和 16 将被忽略。
Ignoring an Unused Variable by Starting Its Name with _
如果您创建了一个变量但没有在任何地方使用它,Rust 通常会发出警告,因为未使用的变量可能是错误。但是,有时能够创建一个您暂时不会使用的变量是有用的,例如在原型设计或刚开始一个项目时。在这种情况下,您可以通过在变量名称前加上下划线来告诉 Rust 不要警告您有关未使用的变量。在清单 19-20 中,我们创建了两个未使用的变量,但当我们编译此代码时,我们应该只收到其中一个变量的警告。
这里我们得到了一个关于未使用变量y
的警告,但没有关于未使用_x
的警告。
请注意,仅使用 _
和使用以下划线开头的名称之间存在细微差别。_x
仍然将值绑定到变量,而 _
则完全不绑定。为了展示这种区别的重要性,列表 19-21 将会给我们一个错误。
我们将收到一个错误,因为 s
值仍然会被移动到 _s
,这阻止了我们再次使用 s
。然而,仅使用下划线本身并不会绑定到该值。列表 19-22 将不会出现任何错误地编译,因为 s
没有被移动到 _
。
这段代码运行得很好,因为我们从未将 s
绑定到任何东西上;它没有被移动。
Ignoring Remaining Parts of a Value with ..
对于包含多个部分的值,我们可以使用 ..
语法来使用特定部分并忽略其余部分,从而避免为每个忽略的值列出下划线。..
模式会忽略我们未在模式其余部分显式匹配的值的任何部分。在清单 19-23 中,我们有一个表示三维空间坐标的 Point
结构体。在 match
表达式中,我们只想操作 x
坐标并忽略 y
和 z
字段的值。
我们列出 x
值,然后只包含 ..
模式。这比列出 y: _
和 z: _
更快捷,特别是在处理具有许多字段的结构体时,只有其中一个或两个字段是相关的。
语法 ..
会扩展为它需要的尽可能多的值。列表 19-24
展示了如何在元组中使用 ..
。
在这个代码中,第一个和最后一个值分别与first
和last
匹配。..
将匹配并忽略中间的所有内容。
然而,使用 ..
必须是明确的。如果不清楚哪些值是用于匹配的,哪些值应该被忽略,Rust 会给我们一个错误。列表 19-25 显示了一个使用 ..
模棱两可的例子,因此它将无法编译。
当我们编译这个示例时,我们得到以下错误:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
在匹配 second
之前,Rust 无法确定元组中有多少个值需要忽略,以及之后还需要忽略多少个值。这段代码可能意味着我们想要忽略 2
,将 second
绑定到 4
,然后忽略 8
、16
和 32
;或者我们想要忽略 2
和 4
,将 second
绑定到 8
,然后忽略 16
和 32
;等等。变量名 second
对 Rust 没有特殊意义,因此由于在这种情况下使用 ..
是模糊的,我们会得到一个编译错误。
使用匹配卫语句的额外条件
一个 匹配卫士 是一个额外的 if
条件,在 match
分支的模式之后指定,该分支被选择的前提是这个条件也必须匹配。匹配卫士对于表达比单独的模式更复杂的想法非常有用。
条件可以使用在模式中创建的变量。列表 19-26 显示了一个 match
,其中第一个分支的模式为 Some(x)
,并且还有一个匹配卫士 if x % 2 == 0
(如果数字是偶数,这将为真)。
这个示例将打印 The number 4 is even
。当 num
与第一个分支中的模式进行比较时,它匹配,因为 Some(4)
匹配 Some(x)
。然后匹配卫士检查 x
除以 2 的余数是否等于 0,因为确实等于 0,所以选择了第一个分支。
如果 num
是 Some(5)
,第一个分支的匹配卫士将为假,因为 5 除以 2 的余数是 1,不等于 0。Rust 然后会转到第二个分支,该分支会匹配,因为第二个分支没有匹配卫士,因此匹配任何 Some
变体。
无法在模式中表达 if x % 2 == 0
这个条件,因此
匹配卫语句使我们能够表达这种逻辑。这种额外表达性的缺点是
当涉及匹配卫语句表达式时,编译器不会尝试检查是否穷尽。
在清单 19-11 中,我们提到可以使用匹配卫语来解决我们的模式遮蔽问题。回想一下,我们在 match
表达式的模式中创建了一个新变量,而不是使用 match
外部的变量。这个新变量意味着我们无法测试外部变量的值。清单 19-27 显示了如何使用匹配卫语来解决这个问题。
这段代码现在将打印 Default case, x = Some(5)
。第二个 match 分支中的模式不会引入一个新的变量 y
来遮蔽外部的 y
,这意味着我们可以在 match 守卫中使用外部的 y
。我们指定模式为 Some(n)
而不是 Some(y)
,后者会遮蔽外部的 y
。这创建了一个新的变量 n
,它不会遮蔽任何东西,因为在 match
外部没有 n
变量。
匹配卫士 if n == y
不是模式,因此不会引入新的变量。这个 y
是 外部的 y
而不是新的被遮盖的 y
,我们可以通过比较 n
和 y
来查找与外部 y
具有相同值的值。
您还可以在匹配卫士中使用 或 操作符 |
来指定多个模式;匹配卫士条件将适用于所有模式。列表 19-28 显示了将使用 |
的模式与匹配卫士结合时的优先级。此示例的重要部分是 if y
匹配卫士适用于 4
、5
和 6
,即使看起来 if y
仅适用于 6
。
匹配条件指出,只有当 x
的值等于 4
、5
或 6
并且 y
为 true
时,该臂才匹配。当此代码运行时,第一个臂的模式匹配因为 x
为 4
,但匹配卫士 if y
为假,所以第一个臂未被选择。代码继续到第二个臂,该臂匹配,此程序打印 no
。原因是 if
条件适用于整个模式 4 | 5 | 6
,而不仅仅是最后一个值 6
。换句话说,匹配卫士相对于模式的优先级行为如下:
(4 | 5 | 6) if y => ...
而不是这个:
4 | 5 | (6 if y) => ...
在运行代码后,优先级行为很明显:如果匹配卫士仅应用于使用 |
操作符指定的值列表中的最后一个值,该臂就会匹配,程序将打印 yes
。
@
绑定
at 操作符 @
让我们可以在测试值是否符合模式的同时创建一个持有该值的变量。在清单 19-29 中,我们希望测试 Message::Hello
的 id
字段是否在范围 3..=7
内。我们还想将该值绑定到变量 id_variable
,以便在与该分支关联的代码中使用。我们可以将此变量命名为 id
,与字段同名,但在这个例子中我们将使用不同的名称。
这个示例将打印 Found an id in range: 5
。通过在范围 3..=7
之前指定 id_variable @
,我们不仅测试了值是否匹配范围模式,还捕获了匹配范围的值。
在第二个分支中,我们只在模式中指定了一个范围,与该分支关联的代码没有包含 id
字段实际值的变量。id
字段的值可能是 10、11 或 12,但与该模式关联的代码不知道具体是哪个值。模式代码无法使用 id
字段的值,因为我们没有将 id
值保存在变量中。
在最后一个分支中,我们指定了一个没有范围的变量,我们确实有值可以在分支的代码中使用,变量名为id
。原因是使用了结构体字段简写语法。但是在这个分支中,我们没有对id
字段中的值进行任何测试,而前两个分支中我们进行了测试:任何值都会匹配这个模式。
使用 @
可以让我们在一个模式中测试一个值并将其保存在一个变量中。
摘要
Rust 的模式非常有用,可以区分不同类型的数据。当在 match
表达式中使用时,Rust 确保您的模式覆盖了所有可能的值,否则程序将无法编译。在 let
语句和函数参数中的模式使这些构造更加有用,能够在分配变量的同时将值解构为更小的部分。我们可以创建简单或复杂的模式以满足我们的需求。
接下来,对于本书的倒数第二章,我们将探讨 Rust 各种特性的一些高级方面。