模式语法
在本节中,我们将收集所有在模式中有效的语法,并讨论为什么以及何时可能想要使用每个语法。
匹配字面量
正如你在第 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
表达式中作为模式的一部分声明的变量会遮蔽 match
结构之外具有相同名称的变量,就像所有变量一样。在清单 18-11 中,我们声明了一个名为 x
的变量,其值为 Some(5)
,以及一个名为 y
的变量,其值为 10
。然后,我们对 x
的值创建了一个 match
表达式。查看 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
表达式运行时发生了什么。第一个 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
。
要创建一个比较外部 x
和 y
值的 match
表达式,而不是引入被遮蔽的变量,我们需要使用 match
保护条件。我们将在 “使用 match
保护条件的额外条件”部分中讨论 match
保护条件。
多个模式
在 match
表达式中,你可以使用 |
语法匹配多个模式,这是模式 或 运算符。例如,在以下代码中,我们将 x
的值与 match
分支匹配,其中第一个分支有一个 或 选项,这意味着如果 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 之间的任何数字!
编译器在编译时检查范围是否为空,并且因为 Rust 唯一可以判断范围是否为空的类型是 char
和数字值,所以范围只允许与数字或 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 显示了一个 Point
结构体,它有两个字段 x
和 y
,我们可以使用带有 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;
包含很多重复,Rust 为匹配结构体字段的模式提供了一种简写方式:你只需要列出结构体字段的名称,从模式中创建的变量将具有相同的名称。清单 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
)或既不位于x
轴也不位于y
轴上的点。
文件名: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})"); } } }
第一个分支将匹配任何位于x
轴上的点,方法是指定y
字段匹配如果其值与字面值0
匹配。该模式仍然创建一个x
变量,我们可以在该分支的代码中使用它。
类似地,第二个分支匹配任何位于y
轴上的点,方法是指定x
字段匹配如果其值为0
,并为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 章中的清单 6-5),但还没有明确讨论解构枚举的模式对应于枚举中存储的数据的定义方式。例如,在清单 18-15 中,我们使用清单 6-2 中的Message
枚举,并编写一个match
,其中包含将解构每个内部值的模式。
文件名: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 中的代码以支持清单 18-16 中所示的ChangeColor
消息中的 RGB 和 HSV 颜色。
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
表达式中指定这些复杂的条件,即使涉及两个枚举。
解构结构体和元组
我们可以以更复杂的方式混合、匹配和嵌套解构模式。以下示例展示了一个复杂的解构,我们在元组中嵌套结构体和元组,并解构所有原始值。
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 通常会发出警告,因为未使用的变量可能是错误。但是,有时能够创建你尚未使用的变量很有用,例如,当你进行原型设计或刚开始一个项目时。在这种情况下,你可以告诉 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 没有任何特殊含义,因此我们得到一个编译器错误,因为在两个地方使用..
是模棱两可的。
使用匹配守卫进行额外条件判断
匹配守卫 是一个额外的if
条件,在match
分支的模式之后指定,该条件也必须匹配才能选择该分支。匹配守卫对于表达比模式本身允许的更复杂的想法很有用。
该条件可以使用在模式中创建的变量。清单 18-26 展示了一个match
,其中第一个分支具有模式Some(x)
,并且还有一个匹配守卫if x % 2 == 0
(如果数字是偶数,则为真)。
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,因为它是,所以选择了第一个分支。
如果num
是Some(5)
而不是Some(4)
,则第一个分支的匹配守卫将为假,因为 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
。我们没有将模式指定为Some(y)
(这会遮蔽外部的y
),而是指定了Some(n)
。这将创建一个新的变量n
,它不会遮蔽任何内容,因为match
外部没有n
变量。
匹配守卫if n == y
不是模式,因此不会引入新变量。这个y
是外部的y
,而不是一个新的被遮蔽的y
,我们可以通过将n
与y
进行比较来查找具有与外部y
相同值的 value。
您也可以在匹配守卫中使用或 运算符|
来指定多个模式;匹配守卫条件将应用于所有模式。清单 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) => ...
在运行代码后,优先级行为很明显:如果匹配守卫仅应用于使用|
运算符指定的 value 列表中的最后一个值,则该分支将匹配,并且程序将打印yes
。
@
绑定
at 运算符@
使我们能够在测试 value 是否匹配模式的同时,创建一个保存该 value 的变量。在清单 18-29 中,我们想要测试Message::Hello
的id
字段是否在范围3..=7
内。我们还想要将该 value 绑定到变量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 @
,我们捕获了与该范围匹配的任何 value,同时还测试了该 value 是否匹配该范围模式。
在第二个分支中,我们只在模式中指定了一个范围,与该分支关联的代码没有包含id
字段实际值的变量。id
字段的值可能是 10、11 或 12,但与该模式相关的代码不知道是哪个。模式代码无法使用id
字段中的 value,因为我们没有将id
value 保存到变量中。
在最后一个分支中,我们指定了一个没有范围的变量,我们确实有一个变量,名为id
,可以在分支的代码中使用。原因是我们使用了结构体字段简写语法。但是,我们没有对该分支的id
字段中的 value 应用任何测试,就像我们在前两个分支中所做的那样:任何 value 都将匹配此模式。
使用@
使我们能够在同一个模式中测试 value 并将其保存到变量中。
总结
Rust 的模式在区分不同类型的数据方面非常有用。当在match
表达式中使用时,Rust 会确保您的模式涵盖所有可能的值,否则您的程序将无法编译。let
语句和函数参数中的模式使这些构造更实用,能够在将值分配给变量的同时,将这些值分解成更小的部分。我们可以创建简单或复杂的模式来满足我们的需求。
接下来,在本书的倒数第二章中,我们将介绍 Rust 的各种功能的一些高级方面。