模式语法
在本节中,我们将收集模式中所有有效的语法,并讨论你可能想要使用每种语法的原因和时机。
匹配字面量
正如你在第 6 章中看到的,你可以直接将模式与字面量进行匹配。以下代码给出了一些示例
这段代码会打印 one
,因为 x
中的值是 1。当你希望你的代码在获得特定的具体值时采取行动时,此语法很有用。
匹配命名变量
命名变量是不可反驳的模式,可以匹配任何值,我们在本书中已经多次使用过它们。但是,当你在 match
、if let
或 while let
表达式中使用命名变量时,会有一个复杂的情况。因为这些表达式中的每一种都会启动一个新的作用域,所以在表达式内部作为模式一部分声明的变量将遮蔽作用域外部具有相同名称的变量,这与所有变量的情况一样。在 Listing 19-11 中,我们声明了一个名为 x
的变量,其值为 Some(5)
,以及一个名为 y
的变量,其值为 10
。然后,我们在值 x
上创建一个 match
表达式。查看 match 分支中的模式和末尾的 println!
,并尝试在运行此代码或进一步阅读之前弄清楚代码将打印什么。
match
表达式,其中一个分支引入了一个新的变量,该变量遮蔽了一个现有的变量 y
让我们逐步了解当 match
表达式运行时会发生什么。第一个 match 分支中的模式与定义的 x
值不匹配,因此代码继续执行。
第二个 match 分支中的模式引入了一个名为 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
。
要创建一个 match
表达式,该表达式比较外部 x
和 y
的值,而不是引入一个新变量来遮蔽现有的 y
变量,我们需要使用匹配守卫条件。我们将在稍后的 “带有匹配守卫的额外条件”章节中讨论匹配守卫。
多个模式
你可以使用 |
语法匹配多个模式,这是模式或运算符。例如,在以下代码中,我们将 x
的值与 match 分支进行匹配,其中第一个分支具有或选项,这意味着如果 x
的值与该分支中的任何一个值匹配,则将运行该分支的代码
这段代码会打印 one or two
。
使用 ..=
匹配值范围
..=
语法允许我们匹配包含指定范围的值。在以下代码中,当模式匹配给定范围内的任何值时,该分支将执行
如果 x
是 1、2、3、4 或 5,则第一个分支将匹配。对于多个匹配值,此语法比使用 |
运算符表达相同的想法更方便;如果我们要使用 |
,我们将不得不指定 1 | 2 | 3 | 4 | 5
。指定范围要简洁得多,特别是当我们想要匹配例如 1 到 1,000 之间的任何数字时!
编译器会检查范围在编译时是否为空,并且因为 Rust 只能判断 char
和数值类型的范围是否为空,所以范围仅允许用于数值或 char
值。
这是一个使用 char
值范围的示例
Rust 可以判断 'c'
在第一个模式的范围内,并打印 early ASCII letter
。
解构以分解值
我们还可以使用模式来解构结构体、枚举和元组,以使用这些值的不同部分。让我们逐步了解每个值。
解构结构体
Listing 19-12 展示了一个带有两个字段 x
和 y
的 Point
结构体,我们可以使用带有 let
语句的模式将其分解。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
这段代码创建了变量 a
和 b
,它们匹配 p
结构体的 x
和 y
字段的值。此示例表明,模式中变量的名称不必与结构体的字段名称匹配。但是,通常将变量名称与字段名称匹配,以便更容易记住哪些变量来自哪些字段。由于这种常见的用法,并且因为编写 let Point { x: x, y: y } = p;
包含大量重复,Rust 为匹配结构体字段的模式提供了一个简写:你只需要列出结构体字段的名称,并且从模式创建的变量将具有相同的名称。Listing 19-13 的行为与 Listing 19-12 中的代码相同,但 let
模式中创建的变量是 x
和 y
而不是 a
和 b
。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
这段代码创建了变量 x
和 y
,它们匹配 p
变量的 x
和 y
字段。结果是变量 x
和 y
包含来自 p
结构体的值。
我们还可以在结构体模式中解构字面量值,而不是为所有字段创建变量。这样做允许我们在测试某些字段的特定值的同时,创建变量来解构其他字段。
在 Listing 19-14 中,我们有一个 match
表达式,它将 Point
值分为三种情况:直接位于 x
轴上的点(当 y = 0
时为真)、位于 y
轴上的点 (x = 0
) 或两者都不是的点。
第一个分支将匹配任何位于 x
轴上的点,方法是指定如果 y
字段的值与字面量 0
匹配,则匹配。该模式仍然创建一个 x
变量,我们可以在此分支的代码中使用它。
类似地,第二个分支匹配任何位于 y
轴上的点,方法是指定如果 x
字段的值为 0
,则匹配,并为 y
字段的值创建一个变量 y
。第三个分支未指定任何字面量,因此它匹配任何其他 Point
,并为 x
和 y
字段创建变量。
在本例中,由于 x
包含 0,因此值 p
与第二个分支匹配,因此这段代码将打印 On the y axis at 7
。
请记住,match
表达式在找到第一个匹配模式后会停止检查分支,因此即使 Point { x: 0, y: 0}
位于 x
轴和 y
轴上,这段代码也只会打印 On the x axis at 0
。
解构枚举
我们在本书中已经解构了枚举(例如,第 6 章中的 Listing 6-5),但尚未明确讨论解构枚举的模式与枚举中存储的数据的定义方式相对应。例如,在 Listing 19-15 中,我们使用来自 Listing 6-2 的 Message
枚举,并编写一个 match
,其模式将解构每个内部值。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change the color to red {r}, green {g}, and blue {b}");
}
}
}
这段代码将打印 Change the color to red 0, green 160, and blue 255
。尝试更改 msg
的值以查看其他分支中的代码运行。
对于没有任何数据的枚举变体,例如 Message::Quit
,我们无法进一步解构该值。我们只能匹配字面量 Message::Quit
值,并且该模式中没有变量。
对于类似结构体的枚举变体,例如 Message::Move
,我们可以使用类似于我们指定用于匹配结构体的模式。在变体名称之后,我们放置花括号,然后列出带有变量的字段,以便我们分解各个部分以在此分支的代码中使用。在这里,我们使用了 Listing 19-13 中的简写形式。
对于类似元组的枚举变体,例如保存一个元素元组的 Message::Write
和保存三个元素元组的 Message::ChangeColor
,模式类似于我们指定用于匹配元组的模式。模式中变量的数量必须与我们正在匹配的变体中元素的数量相匹配。
解构嵌套结构体和枚举
到目前为止,我们的示例都是匹配一层深度的结构体或枚举,但匹配也可以在嵌套项上工作!例如,我们可以重构 Listing 19-15 中的代码,以支持 ChangeColor
消息中的 RGB 和 HSV 颜色,如 Listing 19-16 所示。
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
match
表达式中第一个分支的模式匹配一个包含 Color::Rgb
变体的 Message::ChangeColor
枚举变体;然后模式绑定到三个内部 i32
值。第二个分支的模式也匹配一个 Message::ChangeColor
枚举变体,但内部枚举匹配 Color::Hsv
。我们可以在一个 match
表达式中指定这些复杂的条件,即使涉及到两个枚举。
解构结构体和元组
我们可以以更复杂的方式混合、匹配和嵌套解构模式。以下示例展示了一个复杂的解构,其中我们将结构体和元组嵌套在元组内部,并解构所有原始值
这段代码允许我们将复杂类型分解为其组成部分,以便我们可以单独使用我们感兴趣的值。
使用模式进行解构是一种方便的方式,可以单独使用值的部分,例如结构体中每个字段的值,彼此分开。
忽略模式中的值
你已经看到,有时忽略模式中的值很有用,例如在 match
的最后一个分支中,获得一个捕获所有内容的分支,该分支实际上不执行任何操作,但确实考虑了所有剩余的可能值。有几种方法可以忽略模式中的整个值或部分值:使用 _
模式(你已经见过),在另一个模式中使用 _
模式,使用以下划线开头的名称,或使用 ..
忽略值的其余部分。让我们探讨如何以及为何使用这些模式中的每一种。
使用 _
忽略整个值
我们已经使用下划线作为通配符模式,它将匹配任何值,但不绑定到该值。这在 match
表达式的最后一个分支中尤其有用,但我们也可以在任何模式中使用它,包括函数参数,如 Listing 19-17 所示。
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
_
这段代码将完全忽略作为第一个参数传递的值 3
,并将打印 This code only uses the y parameter: 4
。
在大多数情况下,当你不再需要特定的函数参数时,你将更改签名,使其不包含未使用的参数。忽略函数参数在某些情况下可能特别有用,例如,当你实现一个 trait 时,你需要某种类型签名,但你的实现中的函数体不需要其中一个参数。然后,你可以避免收到关于未使用函数参数的编译器警告,就像你使用名称时会收到警告一样。
使用嵌套的 _
忽略值的部分
我们还可以在另一个模式内部使用 _
来仅忽略值的一部分,例如,当我们只想测试值的一部分,但在我们想要运行的相应代码中不需要其他部分时。Listing 19-18 展示了负责管理设置值的代码。业务需求是,不允许用户覆盖对设置的现有自定义,但如果设置当前未设置,则可以取消设置并为其赋值。
Some
变体内部的值时,在匹配 Some
变体的模式中使用下划线这段代码将打印 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
。
我们还可以在一个模式中的多个位置使用下划线来忽略特定值。Listing 19-19 展示了一个忽略包含五个项的元组中第二个和第四个值的示例。
这段代码将打印 Some numbers: 2, 8, 32
,并且值 4 和 16 将被忽略。
通过以下划线开头来忽略未使用的变量
如果你创建一个变量但不在任何地方使用它,Rust 通常会发出警告,因为未使用的变量可能是一个错误。但是,有时能够创建一个你尚未使用,例如当你正在进行原型设计或刚开始一个项目时,会很有用。在这种情况下,你可以通过以下划线开头变量的名称来告诉 Rust 不要警告你关于未使用的变量。在 Listing 19-20 中,我们创建了两个未使用的变量,但是当我们编译这段代码时,我们应该只收到关于其中一个变量的警告。
fn main() {
let _x = 5;
let y = 10;
}
在这里,我们收到了关于未使用变量 y
的警告,但我们没有收到关于未使用 _x
的警告。
请注意,仅使用 _
和使用以下划线开头的名称之间存在细微差别。语法 _x
仍然将值绑定到变量,而 _
则根本不绑定。为了展示这种区别很重要的情况,Listing 19-21 将为我们提供一个错误。
我们将收到一个错误,因为 s
值仍将被移动到 _s
中,这会阻止我们再次使用 s
。但是,单独使用下划线永远不会绑定到该值。Listing 19-22 将在没有任何错误的情况下编译,因为 s
没有被移动到 _
中。
这段代码可以正常工作,因为我们从不将 s
绑定到任何东西;它不会被移动。
使用 ..
忽略值的剩余部分
对于具有许多部分的值,我们可以使用 ..
语法来使用特定部分并忽略其余部分,从而避免为每个忽略的值列出下划线。..
模式忽略我们在模式的其余部分中未显式匹配的任何值部分。在 Listing 19-23 中,我们有一个 Point
结构体,它保存三维空间中的坐标。在 match
表达式中,我们只想对 x
坐标进行操作,并忽略 y
和 z
字段中的值。
..
忽略 Point
的所有字段,除了 x
我们列出 x
值,然后只包含 ..
模式。这比必须列出 y: _
和 z: _
更快,特别是当我们处理具有许多字段的结构体,而在某些情况下只有一两个字段相关时。
语法 ..
将扩展到所需的尽可能多的值。Listing 19-24 展示了如何在元组中使用 ..
。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
在这段代码中,第一个和最后一个值与 first
和 last
匹配。..
将匹配并忽略中间的所有内容。
但是,使用 ..
必须是明确的。如果不清楚哪些值用于匹配,哪些值应该被忽略,Rust 将会给我们一个错误。Listing 19-25 展示了一个以不明确的方式使用 ..
的示例,因此它将无法编译。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
..
当我们编译此示例时,我们得到此错误
$ 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
Rust 不可能确定在与 second
匹配值之前要忽略元组中的多少个值,然后在之后要忽略多少个值。这段代码可能意味着我们想要忽略 2
,将 second
绑定到 4
,然后忽略 8
、16
和 32
;或者我们想要忽略 2
和 4
,将 second
绑定到 8
,然后忽略 16
和 32
;等等。变量名 second
对 Rust 没有任何特殊意义,因此我们收到编译器错误,因为像这样在两个位置使用 ..
是不明确的。
带有匹配守卫的额外条件
匹配守卫是一个额外的 if
条件,在 match
分支中的模式之后指定,该条件也必须匹配才能选择该分支。匹配守卫对于表达比单独模式允许的更复杂的想法很有用。它们仅在 match
表达式中可用,而不能在 if let
或 while let
表达式中使用。
该条件可以使用在模式中创建的变量。Listing 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)
而不是 Some(4)
,则第一个分支中的匹配守卫将为 false,因为 5 除以 2 的余数为 1,不等于 0。然后 Rust 将转到第二个分支,第二个分支将匹配,因为第二个分支没有匹配守卫,因此匹配任何 Some
变体。
没有办法在模式中表达 if x % 2 == 0
条件,因此匹配守卫使我们能够表达这种逻辑。这种额外表达能力的缺点是,当涉及匹配守卫表达式时,编译器不会尝试检查穷尽性。
在 Listing 19-11 中,我们提到我们可以使用匹配守卫来解决我们的模式遮蔽问题。回想一下,我们在 match
表达式中的模式内部创建了一个新变量,而不是使用 match
外部的变量。这个新变量意味着我们无法针对外部变量的值进行测试。Listing 19-27 展示了我们如何使用匹配守卫来解决此问题。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
这段代码现在将打印 Default case, x = Some(5)
。第二个 match 分支中的模式没有引入一个新的变量 y
来遮蔽外部的 y
,这意味着我们可以在匹配守卫中使用外部的 y
。我们没有将模式指定为 Some(y)
,这将遮蔽外部的 y
,而是指定了 Some(n)
。这创建了一个新的变量 n
,它不会遮蔽任何内容,因为 match
外部没有 n
变量。
匹配守卫 if n == y
不是模式,因此不引入新变量。这个 y
是外部的 y
,而不是一个新的 y
遮蔽它,我们可以通过比较 n
和 y
来查找具有与外部 y
相同的值的值。
你还可以在匹配守卫中使用或运算符 |
来指定多个模式;匹配守卫条件将应用于所有模式。Listing 19-28 展示了将使用 |
的模式与匹配守卫组合时的优先级。此示例的重要部分是 if y
匹配守卫应用于 4
、5
和 6
,即使它看起来好像 if y
仅应用于 6
。
匹配条件说明,仅当 x
的值等于 4
、5
或 6
并且 y
为 true
时,该分支才匹配。当此代码运行时,第一个分支的模式匹配,因为 x
为 4
,但是匹配守卫 if y
为 false,因此未选择第一个分支。代码继续执行到第二个分支,第二个分支确实匹配,并且此程序打印 no
。原因是 if
条件应用于整个模式 4 | 5 | 6
,而不仅仅是最后一个值 6
。换句话说,匹配守卫相对于模式的优先级行为如下
(4 | 5 | 6) if y => ...
而不是这样
4 | 5 | (6 if y) => ...
运行代码后,优先级行为很明显:如果匹配守卫仅应用于使用 |
运算符指定的值列表中的最后一个值,则该分支将匹配,并且程序将打印 yes
。
@
绑定
at 运算符 @
允许我们创建一个变量,该变量在我们在测试该值是否与模式匹配的同时保存一个值。在 Listing 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 各种功能的一些高级方面。