所有的模式语法
ch19-03-pattern-syntax.md
commit 6fce661a0938aa0da06526e7b8f98fd7e67a222f
在本节中,我们收集了模式中所有有效的语法,并讨论为什么以及何时你可能要使用这些语法。
匹配字面值
如第六章所示,可以直接匹配字面值模式。如下代码给出了一些例子:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
这段代码会打印 one
因为 x
的值是 1。如果希望代码获得特定的具体值,则该语法很有用。
匹配命名变量
命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于 match
表达式时情况会有些复杂。因为 match
会开始一个新作用域,match
表达式中作为模式的一部分声明的变量会覆盖 match
结构之外的同名变量,与所有变量一样。在示例 18-11 中,声明了一个值为 Some(5)
的变量 x
和一个值为 10
的变量 y
。接着在值 x
上创建了一个 match
表达式。观察匹配分支中的模式和结尾的 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}"); }
让我们看看当 match
语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 x
中定义的值,所以代码继续执行。
第二个匹配分支中的模式引入了一个新变量 y
,它会匹配任何 Some
中的值。因为我们在 match
表达式的新作用域中,这是一个新变量,而不是开头声明为值 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 guard)。我们稍后将在 “匹配守卫提供的额外条件” 这一小节讨论匹配守卫。
多个模式
在 match
表达式中,可以使用 |
语法匹配多个模式,它代表 或(or)运算符模式。例如,如下代码将 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 到 1000 的数字的时候!
编译器会在编译时检查范围不为空,而 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
。
解构并分解值
也可以使用模式来解构结构体、枚举和元组,以便使用这些值的不同部分。让我们来分别看一看。
解构结构体
示例 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); }
这段代码创建了变量 a
和 b
来匹配结构体 p
中的 x
和 y
字段。这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p;
包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。示例 18-13 展示了与示例 18-12 有着相同行为的代码,不过 let
模式创建的变量为 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); }
这段代码创建了变量 x
和 y
,与变量 p
中的 x
和 y
相匹配。其结果是变量 x
和 y
包含结构体 p
中的值。
也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。
示例 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})"); } } }
第一个分支通过指定字段 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
。
解构枚举
本书之前曾经解构过枚举(例如第六章示例 6-5),不过当时没有明确提到解构枚举的模式需要对应枚举所定义的储存数据的方式。让我们以示例 6-2 中的 Message
枚举为例,编写一个 match
使用模式解构每一个内部值,如示例 18-15 所示:
文件名: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}") } } }
这段代码会打印出 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 色彩模式,如示例 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}") } _ => (), } }
match
表达式第一个分支的模式匹配一个包含 Color::Rgb
枚举成员的 Message::ChangeColor
枚举成员,然后模式绑定了 3 个内部的 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 }); }
这将复杂的类型分解成部分组件以便可以单独使用我们感兴趣的值。
通过模式解构是一个方便利用部分值片段的手段,比如结构体中每个单独字段的值。
忽略模式中的值
有时忽略模式中的一些值是有用的,比如 match
中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值:使用 _
模式(我们已经见过了),在另一个模式中使用 _
模式,使用一个以下划线开始的名称,或者使用 ..
忽略所剩部分的值。让我们来分别探索如何以及为什么要这么做。
使用 _
忽略整个值
我们已经使用过下划线作为匹配但不绑定任何值的通配符模式了。虽然这作为 match
表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中,如示例 18-17 所示:
文件名:src/main.rs
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 时,当你需要特定类型签名但是函数实现并不需要某个参数时。这样可以避免一个存在未使用的函数参数的编译警告,就跟使用命名参数一样。
使用嵌套的 _
忽略部分值
也可以在一个模式内部使用_
忽略部分值,例如,当只需要测试部分值但在期望运行的代码中没有用到其他部分时。示例 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:?}"); }
这段代码会打印出 Can't overwrite an existing customized value
接着是 setting is Some(5)
。在第一个匹配分支,我们不需要匹配或使用任一个 Some
成员中的值;重要的部分是需要测试 setting_value
和 new_setting_value
都为 Some
成员的情况。在这种情况,我们打印出为何不改变 setting_value
,并且不会改变它。
对于所有其他情况(setting_value
或 new_setting_value
任一为 None
),这由第二个分支的 _
模式体现,这时确实希望允许 new_setting_value
变为 setting_value
。
也可以在一个模式中的多处使用下划线来忽略特定值,如示例 18-19 所示,这里忽略了一个五元元组中的第二和第四个值:
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}") } } }
这会打印出 Some numbers: 2, 8, 32
,值 4 和 16 会被忽略。
通过在名字前以一个 _
开头来忽略未使用的变量
如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为未使用的变量可能会是个 bug。但是有时创建一个还未使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头。示例 18-20 中创建了两个未使用变量,不过当编译代码时只会得到其中一个的警告:
文件名:src/main.rs
fn main() { let _x = 5; let y = 10; }
这里得到了警告说未使用变量 y
,不过没有警告说使用 _x
。
注意,只使用 _
和使用以下划线开头的名称有些微妙的不同:比如 _x
仍会将值绑定到变量,而 _
则完全不会绑定。为了展示这个区别的意义,示例 18-21 会产生一个错误。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
我们会得到一个错误,因为 s
的值仍然会移动进 _s
,并阻止我们再次使用 s
。然而只使用下划线本身,并不会绑定值。示例 18-22 能够无错编译,因为 s
没有被移动进 _
:
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }
上面的代码能很好的运行;因为没有把 s
绑定到任何变量;它没有被移动。
用 ..
忽略剩余值
对于有多个部分的值,可以使用 ..
语法来只使用特定部分并忽略其它值,同时避免不得不每一个忽略值列出下划线。..
模式会忽略模式中剩余的任何没有显式匹配的值部分。在示例 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}"), } }
这里列出了 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}"); } } }
这里用 first
和 last
来匹配第一个和最后一个值。..
将匹配并忽略中间的所有值。
然而使用 ..
必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。示例 18-25 展示了一个带有歧义的 ..
例子,因此其不能编译:
文件名:src/main.rs
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 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 ..
是有歧义的。
匹配守卫提供的额外条件
匹配守卫(match guard)是一个指定于 match
分支模式之后的额外 if
条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。
这个条件可以使用模式中创建的变量。示例 18-26 展示了一个 match
,其中第一个分支有模式 Some(x)
还有匹配守卫 if x % 2 == 0
(当 x
是偶数的时候为真):
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 => (), } }
上例会打印出 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
的条件,所以通过匹配守卫提供了表达类似逻辑的能力。这种替代表达方式的缺点是,编译器不会尝试为包含匹配守卫的模式检查穷尽性。
在示例 18-11 中,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 match
表达式的模式中新建了一个变量而不是使用 match
之外的同名变量。新变量意味着不能够测试外部变量的值。示例 18-27 展示了如何使用匹配守卫修复这个问题。
文件名: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}"); }
现在这会打印出 Default case, x = Some(5)
。现在第二个匹配分支中的模式不会引入一个覆盖外部 y
的新变量 y
,这意味着可以在匹配守卫中使用外部的 y
。相比指定会覆盖外部 y
的模式 Some(y)
,这里指定为 Some(n)
。此新建的变量 n
并没有覆盖任何值,因为 match
外部没有变量 n
。
匹配守卫 if n == y
并不是一个模式所以没有引入新变量。这个 y
正是 外部的 y
而不是新的覆盖变量 y
,这样就可以通过比较 n
和 y
来表达寻找一个与外部 y
相同的值的概念了。
也可以在匹配守卫中使用 或 运算符 |
来指定多个模式,同时匹配守卫的条件会作用于所有的模式。示例 18-28 展示了结合匹配守卫与使用了 |
的模式的优先级。这个例子中重要的部分是匹配守卫 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"), } }
这个匹配条件表明此分支值匹配 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 运算符(@
)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。示例 18-29 展示了一个例子,这里我们希望测试 Message::Hello
的 id
字段是否位于 3..=7
范围内,同时也希望能将其值绑定到 id_variable
变量中以便此分支相关联的代码可以使用它。可以将 id_variable
命名为 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}"), } }
上例会打印出 Found an id in range: 5
。通过在 3..=7
之前指定 id_variable @
,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。
第二个分支只在模式中指定了一个范围,分支相关代码没有一个包含 id
字段实际值的变量。id
字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id
字段中的值,因为没有将 id
值保存进一个变量。
最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id
,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id
字段的值进行测试:任何值都会匹配此分支。
使用 @
可以在一个模式中同时测试和保存变量值。
总结
模式是 Rust 中一个很有用的功能,它有助于我们区分不同类型的数据。当用于 match
语句时,Rust 确保模式会包含每一个可能的值,否则程序将不能编译。let
语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。
接下来,在本书倒数第二章中,我们将介绍一些 Rust 众多功能中较为高级的部分。