模式语法
在本节中,我们收集了模式中所有有效的语法,并讨论了您可能希望使用每种语法的原因和时间。
匹配文本
正如你在第 6 章中看到的,你可以直接将模式与文本匹配。以下代码给出了一些示例:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
此代码打印 1
,因为 x
中的值为 1。当您希望代码在获得特定具体值时执行作时,此语法非常有用。
匹配命名变量
命名变量是匹配任何值的无可辩驳的模式,我们在书中已经多次使用它们。但是,当您在 match
表达式中使用命名变量时,会很复杂。由于 match
会启动一个新的范围,因此在 match
表达式中声明为模式一部分的变量将隐藏在 match
结构外部具有相同名称的变量,就像所有变量一样。在示例 18-11 中,我们声明了一个名为 x
的变量,其值为 Some(5)
和一个变量 y
,其值为 10
。然后,我们创建一个
match
表达式。
查看 match arms 和
println!
最后,并尝试在运行此代码或进一步阅读之前弄清楚代码将打印什么。
文件名: src/main.rs
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {y}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
示例 18-11:带有 arm 的 match
表达式,引入了阴影变量 y
让我们来看看 match
表达式运行时会发生什么。第一个 match arm 中的模式与定义的 x
值不匹配,因此代码将继续。
第二个匹配臂中的模式引入了一个名为 y
的新变量,该变量将匹配 Some
值中的任何值。因为我们在 match
表达式中的新范围内,所以这是一个新的 y
变量,而不是我们在开头声明的值为 10 的 y
。这个新的 y
绑定将匹配 Some
中的任何值,这就是我们在 x
中拥有的值。因此,这个新的 y
绑定到 x
中 Some
的内部值。该值为 5
,因此该臂的表达式将执行并打印 Matched, y = 5
。
如果 x
是 None
值而不是 Some(5),
则前两个分支中的模式不会匹配,因此该值将与下划线匹配。我们没有在下划线 arm 的模式中引入 x
变量,因此表达式中的 x
仍然是没有被遮蔽的外部 x
。在此假设情况下,匹配项
将打印 Default case, x = None
。
当 match
表达式完成后,它的作用域结束,内部 y
的作用域也结束。最后一个 println!
生成 at the end: x = Some(5), y = 10
。
创建比较外部
x
和
y
,而不是引入阴影变量,我们需要使用 match guard 条件。我们将在后面的“Extra
Conditionals with Match Guards“ 部分。
多种模式
在 match
表达式中,您可以使用 |
语法(即 pattern 或 operator)匹配多个模式。例如,在下面的代码中,我们将 x
的值与匹配的 Arms 进行匹配,其中第一个 Arm 有一个 or 选项,这意味着如果 x
的值与该 Arm 中的任何一个值匹配,则该 Arm 的代码将运行:
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
此代码打印 1 或 2
。
将值范围与 .. 匹配=
这 ..=
语法允许我们匹配一个包含的值范围。在下面的代码中,当模式与给定范围内的任何值匹配时,该分支将执行:
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 之间的任何数字时!
编译器在编译时检查范围是否为空,并且因为 Rust 可以判断范围是否为空的唯一类型是 char
和 numeric 值,所以范围只允许有 numeric 或 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'
在第一个模式的范围内,并打印早期的 ASCII 字母
。
解构以分离值
我们还可以使用 patterns 来解构结构体、枚举和 Tuples,以使用这些值的不同部分。让我们来看看每个值。
解构结构体
示例 18-12 展示了一个带有两个字段 x
和 y
的 Point
结构体,我们可以使用带有 let
语句的模式将其拆分。
文件名: src/main.rs
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); }
示例 18-12:将结构体的字段解构为单独的变量
此代码创建与 x
的值匹配的变量 a
和 b
和 p
结构的 y
字段。此示例显示模式中变量的名称不必与结构体的字段名称匹配。但是,将变量名称与字段名称匹配是很常见的,以便更容易记住哪些变量来自哪些字段。由于这种常见的用法,并且因为编写 let Point { x: x, y: y } = p;
包含大量重复,Rust 对匹配结构体字段的模式有一个简写:你只需要列出结构体字段的名称,从该模式创建的变量将具有相同的名称。示例 18-13 的行为与示例 18-12 中的代码相同,但在 let
中创建的变量
pattern 是 x
和 y
,而不是 a
和 b
。
文件名: src/main.rs
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); }
示例 18-13:使用 struct field 简写解构 struct fields
此代码创建与 p
变量的 x
和 y
字段匹配的变量 x
和 y
。结果是变量 x
和 y
包含 p
结构中的值。
我们还可以将 Literals 值作为 struct 模式的一部分进行解构,而不是为所有字段创建变量。这样做允许我们测试某些字段的特定值,同时创建变量来解构其他字段。
在示例 18-14 中,我们有一个 match
表达式,它将 Point
值分为三种情况:直接位于 x
轴上的点(当
y = 0
)、y 轴 (
x = 0
) 或两者都不。
文件名: src/main.rs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => { println!("On neither axis: ({x}, {y})"); } } }
示例 18-14:在一个模式中解构和匹配 Literals 值
第一个分支将匹配位于 x
轴上的任何点,方法是指定 y
字段匹配(如果其值与文本 0
匹配)。该模式仍然会创建一个 x
变量,我们可以在此 Arm 的代码中使用该变量。
同样,第二个分支通过指定 x
字段的值为 0
时匹配 y 轴上的任何点,并为 y
字段的值创建变量 y
,从而匹配 y
轴上的任何点。第三个分支不指定任何文本,因此它与任何其他 Point
匹配,并为 x
和 y
字段创建变量。
在此示例中,值 p
凭借 x
与第二个分支匹配
包含 0,因此此代码将在 7 处的 y 轴
上打印。
请记住,匹配
表达式在找到第一个匹配模式后会停止检查臂,因此即使点 { x: 0, y: 0}
在 x
轴和 y
轴上,此代码也只会在 x 轴上打印 0
。
解构枚举
我们在本书中解构了枚举(例如,第 6 章中的示例 6-5),但尚未明确讨论解构枚举的模式对应于定义存储在枚举中的数据的方式。例如,在示例 18-15 中,我们使用示例 6-2 中的 Message
枚举,并编写一个带有模式的匹配项
,该模式将解构每个内部值。
文件名: src/main.rs
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}") } } }
示例 18-15:解构保存不同类型值的枚举变体
此代码将打印 Change the color to red 0, green 160, and blue 255
。尝试更改 msg
的值以查看来自其他分支的代码运行。
对于没有任何数据的枚举变体,例如 Message::Quit
,我们无法进一步解构该值。我们只能匹配文字 Message::Quit
值,并且该模式中没有变量。
对于类似结构体的枚举变体,例如 Message::Move
,我们可以使用类似于我们指定的模式来匹配结构体的模式。在变体名称之后,我们放置大括号,然后列出带有变量的字段,以便我们拆分要在此分支的代码中使用的部分。这里我们使用简写形式,就像我们在示例 18-13 中所做的那样。
对于类似元组的枚举变体,例如 Message::Write
保存一个具有一个元素的元组,以及 Message::ChangeColor
保存一个具有三个元素的元组,其模式类似于我们指定以匹配元组的模式。模式中的变量数量必须与我们正在匹配的变体中的元素数量匹配。
解构嵌套结构体和枚举
到目前为止,我们的例子都是匹配一层深的结构体或枚举,但匹配也可以用于嵌套项目!例如,我们可以重构示例 18-15 中的代码,以支持 ChangeColor
中的 RGB 和 HSV 颜色
message 中,如示例 18-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}") } _ => (), } }
示例 18-16:匹配嵌套枚举
match
表达式中第一个分支的模式与
Message::ChangeColor
包含 Color::Rgb
变体的枚举变体;然后 pattern 绑定到三个内部 i32
值。第二个分支的模式也匹配 Message::ChangeColor
枚举变体,但内部枚举匹配 Color::Hsv
。我们可以在一个
match
表达式,即使涉及两个枚举。
解构结构和元组
我们可以以更复杂的方式混合、匹配和嵌套解构模式。以下示例显示了一个复杂的解构,其中我们将结构和元组嵌套在一个元组中,并将所有基元值解构出来:
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
这段代码允许我们将复杂类型分解为它们的组成部分,以便我们可以单独使用我们感兴趣的值。
使用模式解构是一种将值片段(例如结构中每个字段的值)彼此分开使用的便捷方法。
忽略模式中的值
您已经看到,有时忽略模式中的值(例如在匹配项
的最后一个分支中)很有用,以获得实际上不执行任何作但确实考虑了所有剩余可能值的 catchall。有几种方法可以忽略模式中的整个值或部分值:使用 _
pattern(您已经看到),在另一个模式中使用 _
模式,使用以下划线开头的名称,或使用 ..
忽略值的其余部分。让我们探讨一下如何以及为什么使用这些模式。
使用 _
忽略整个值
我们使用下划线作为通配符模式,它将匹配任何值,但不会绑定到该值。这在比赛中作为最后一
只手臂特别有用
表达式,但我们也可以在任何模式中使用它,包括函数
参数,如示例 18-17 所示。
文件名: src/main.rs
fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {y}"); } fn main() { foo(3, 4); }
示例 18-17:在函数签名中使用 _
此代码将完全忽略作为第一个参数传递的值 3
,并将打印 This code only uses the y parameter: 4
.
在大多数情况下,当您不再需要某个特定的函数参数时,您可以更改签名,使其不包含未使用的参数。忽略函数参数在以下情况下特别有用,例如,当你在实现某个类型签名时,当你需要某个类型签名,但你的实现中的函数体不需要其中一个参数时。然后,您可以避免收到有关未使用函数参数的编译器警告,就像改用名称一样。
忽略具有嵌套 _
的值的部分
我们还可以在另一个模式中使用 _
来忽略值的一部分,例如,当我们只想测试值的一部分,但对要运行的相应代码中的其他部分没有用处时。示例 18-18 显示了负责管理设置值的代码。业务要求是,不应允许用户覆盖设置的现有自定义,但可以取消设置该设置,并在当前未设置时为其指定一个值。
fn main() { let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {setting_value:?}"); }
示例 18-18:在匹配某些
变体的模式中使用下划线时,我们不需要在
一些
此代码将打印 Can't overwrite an existing customized value
出来,然后
设置为 Some(5)。
在第一个匹配臂中,我们不需要匹配或使用 Some
变体中的值,但我们确实需要测试 setting_value
和 new_setting_value
是 Some
变体的情况。在这种情况下,我们会打印不更改 setting_value
的原因,并且不会更改。
在所有其他情况下(如果 setting_value
或 new_setting_value
无
)由第二条分支中的 _
模式表示,我们希望允许
new_setting_value
成为setting_value
。
我们还可以在一个模式中的多个位置使用下划线来忽略特定值。示例 18-19 显示了一个忽略 5 个项目的元组中的第二个和第四个值的示例。
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}") } } }
示例 18-19:忽略元组的多个部分
此代码将打印一些数字:2、8、32
,值 4 和 16 将被忽略。
通过名称以 _
开头来忽略未使用的变量
如果你创建了一个变量但没有在任何地方使用它,Rust 通常会发出警告,因为未使用的变量可能是一个错误。但是,有时能够创建您尚不会使用的变量会很有用,例如当您正在进行原型设计或刚刚开始项目时。在这种情况下,你可以通过以下划线开头的变量名称来告诉 Rust 不要警告你未使用的变量。在示例 18-20 中,我们创建了两个未使用的变量,但是当我们编译这段代码时,我们应该只收到关于其中一个的警告。
文件名: src/main.rs
fn main() { let _x = 5; let y = 10; }
示例 18-20:以下划线开头的变量名称以避免收到未使用的变量警告
在这里,我们收到有关未使用变量 y
的警告,但未收到有关未使用 _x
的警告。
请注意,仅使用 _
与使用以下划线开头的名称之间存在细微差别。语法 _x
仍然将值绑定到变量,而 _
根本不绑定。为了说明这种区别很重要的情况,示例 18-21 将为我们提供一个错误。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
示例 18-21:以下划线开头的未使用变量仍然绑定值,这可能会获得值的所有权
我们会收到一个错误,因为 s
值仍将移动到 _s
中,这会阻止我们再次使用 s
。但是,单独使用下划线永远不会绑定到该值。示例 18-22 编译时不会出现任何错误,因为 s
没有被移动到 _
中。
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }
示例 18-22:使用下划线不会绑定值
这段代码工作得很好,因为我们从不将 s
绑定到任何东西;它不会移动。
使用 ..
忽略值的剩余部分
对于具有许多部分的值,我们可以使用 ..
语法来使用特定部分并忽略其余部分,从而避免为每个忽略的值列出下划线。..
模式会忽略值中我们尚未忽略的部分
在 pattern 的其余部分显式匹配。在示例 18-23 中,我们有一个
在
三维空间中保存坐标的 Point 结构。在
match
表达式,我们只想对 x
坐标进行作,而忽略 y
和 z
字段中的值。
fn main() { struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {x}"), } }
示例 18-23:使用 ..
我们列出 x
值,然后只包含 ..
模式。这比列出 y: _
和 z: _
要快得多,特别是当我们在只有一个或两个字段相关的情况下使用具有大量字段的结构体时。
语法 ..
将扩展为所需的任意数量的值。示例 18-24 展示了如何对元组使用 ..
。
文件名: src/main.rs
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }
示例 18-24:仅匹配元组中的第一个和最后一个值,而忽略所有其他值
在此代码中,first 和 last 值与 first
和 last
匹配。这
..
将匹配并忽略中间的所有内容。
但是,使用 ..
必须明确。如果不清楚哪些值是用来匹配的,哪些应该被忽略,Rust 会给我们一个错误。示例 18-25 显示了一个模棱两可地使用 ..
的示例,因此它不会编译。
文件名: src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
示例 18-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
Rust 无法确定在将值与 second
匹配之前要忽略 Tuples 中的多少个值,然后确定此后要忽略多少个值。这段代码可能意味着我们想忽略 2,bind
second
到 4
,然后忽略 8
、16
和 32
;或者我们想忽略的
2
和 4
,将 second
绑定到 8
,然后忽略 16
和 32
;等等。变量名称 second
对 Rust 来说没有任何特殊意义,所以我们得到一个编译器错误,因为在这样的两个地方使用 ..
是模棱两可的。
带有 Match Guard 的额外条件语句
match guard 是一个附加的 if
条件,在 match
arm 中的 pattern 之后指定,它也必须匹配才能选择该 arm。match guards 对于表达比单独的 pattern 所允许的更复杂的想法很有用。
条件可以使用在模式中创建的变量。示例 18-26 显示了一个
match
中,第一个分支的模式为 Some(x),
并且还有一个 if x % 2 == 0
的匹配守卫(如果数字为偶数,则为 true)。
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {x} is even"), Some(x) => println!("The number {x} is odd"), None => (), } }
示例 18-26:向模式添加 match 守卫
此示例将打印 The number 4 is even
。当 num
与第一个分支中的模式进行比较时,它会匹配,因为 Some(4)
匹配 Some(x)。
然后,match guard 检查除以 x
除以 2 的余数是否等于 0,因为是,所以选择了第一个分支。
如果 num
是 Some(5),
那么第一个分支中的匹配守卫将是 false,因为 5 除以 2 的余数是 1,它不等于 0。然后 Rust 将转到第二个分支,这将匹配,因为第二个分支没有 match 守卫,因此匹配任何 Some
变体。
在模式中没有办法表达 if x % 2 == 0
条件,所以 match 守卫给了我们表达这个逻辑的能力。这种额外的表达性的缺点是,当涉及 match guard 表达式时,编译器不会尝试检查穷举性。
在示例 18-11 中,我们提到了我们可以使用 match 守卫来解决我们的模式阴影问题。回想一下,我们在 match
表达式的模式内部创建了一个新变量,而不是在
match 的 Match
。这个新变量意味着我们无法测试外部变量的值。示例 18-27 展示了我们如何使用 match 守卫来解决这个问题。
文件名: src/main.rs
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}"); }
示例 18-27:使用 match 守卫测试外部变量的相等性
这段代码现在将打印 Default case, x = Some(5)。
第二个 match 臂中的 pattern 没有引入一个会遮蔽外部 y
的新变量 y
,这意味着我们可以在 match 守卫中使用外部 y
。我们没有将模式指定为 Some(y),
否则会遮蔽外部 y
,而是指定
一些 (n)
。这将创建一个新的变量 n
,它不会隐藏任何内容,因为匹配
之外没有 n
变量。
match guard if n == y
不是一个模式,因此不会引入新的变量。这个 y
是外部 y
而不是新的阴影 y
,我们可以通过比较来查找与外部 y
具有相同值的值
n
到 y
。
您还可以在 match 守卫中使用 or 运算符 |
来指定多个模式;match guard 条件将应用于所有模式。示例 18-28 显示了将使用 |
的模式与 match 守卫相结合时的优先级。这个例子的重要部分是 if y
匹配守卫适用于 4
、5
和6
,即使它可能看起来像 if y
仅适用于 6
。
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
示例 18-28:使用 match 守卫组合多个模式
匹配条件规定,仅当 x
的值等于 4
、5
或 6
且 y
为 true
时,臂才匹配。当此代码运行时,第一个分支的模式匹配,因为 x
为 4
,但如果 y
为 false,因此不会选择第一个分支。代码移动到第二个分支
哪个匹配,并且此程序打印 NO
。原因是 if
condition 适用于整个模式 4 | 5 | 6
,不仅适用于最后一个值
6
. 换句话说,match 守卫相对于模式的优先级是这样的:
(4 | 5 | 6) 如果 y => ...
而不是这样:
4 | 5 |(6 如果 y) => ...
运行代码后,优先行为很明显:如果 match 守卫
仅应用于使用
|
运算符,则 arm 将匹配,并且程序将打印
是的
。
@
绑定
at 运算符 @
允许我们创建一个变量,该变量在测试该值以进行模式匹配的同时保存该值。在示例 18-29 中,我们想测试 Message::Hello
id
字段是否在 3..=7
范围内。我们还希望将该值绑定到变量 id_variable
,以便我们可以在与 arm 关联的代码中使用它。我们可以将此变量命名为 id
,与字段相同,但在此示例中,我们将使用不同的名称。
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7, } => println!("Found an id in range: {id_variable}"), Message::Hello { id: 10..=12 } => { println!("Found an id in another range") } Message::Hello { id } => println!("Found some other id: {id}"), } }
示例 18-29:使用 @
绑定到模式中的值,同时对其进行测试
此示例将打印 Found an id in range: 5
. 通过在范围 3..=7
之前指定 id_variable @
,我们将捕获与范围匹配的任何值,同时测试该值是否与范围模式匹配。
在第二个 Arm 中,我们只在 pattern 中指定了 Range,与该 Arm 关联的代码没有包含 id
字段实际值的变量。id
字段的值可以是 10、11 或 12,但与该模式对应的代码不知道它是哪个。模式代码无法使用 id
字段中的值,因为我们没有保存
id
值。
在最后一个 Arm 中,我们指定了一个没有范围的变量,我们确实在名为 id
的变量中提供了可在 Arm 代码中使用的值。原因是我们使用了 struct field 简写语法。但是我们没有像对前两个分支所做的那样,对这个分支的 id
字段中的值应用任何测试:任何值都会匹配这个模式。
使用 @
可以让我们测试一个值并将其保存在一个模式中的变量中。
总结
Rust 的模式在区分不同类型的数据方面非常有用。当用于 match
表达式时,Rust 确保你的模式覆盖所有可能的值,否则你的程序将无法编译。let
语句和函数参数中的模式使这些结构更加有用,从而能够在分配给变量的同时将值解构为更小的部分。我们可以创建简单或复杂的模式来满足我们的需要。
接下来,在本书的倒数第二章,我们将了解 Rust 的各种功能的一些高级方面。