Rust 学习笔记:模式的语法
Rust 学习笔记:模式的语法
匹配字面值
可以直接将模式与字面量进行匹配。
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
匹配命名变量
命名变量是匹配任何值的不可反驳的模式。但是,当在 match、if let 或 while let 表达式中使用命名变量时,会出现一个复杂问题。因为这类表达式中的每一种都开始一个新的作用域,所以在表达式内部作为模式的一部分声明的变量将在外部遮蔽同名的变量,所有变量都是如此。
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}");
输出:
Matched, y = 5
at the end: x = Some(5), y = 10
第一个匹配分支中的模式与定义的 x 值不匹配,因此代码继续。
第二个匹配分支中的模式引入了一个名为 y 的新变量,该变量将匹配 Some 中的任何值。因为我们在匹配表达式的新范围内,这是一个新的 y 变量,而不是我们在开始时用值 10 声明的 y。这个新的 y 绑定将匹配 Some 中的任何值,也就是我们在 x 中的值,因此该值为 5。
匹配表达式完成后,它的作用域结束,内部 y 的作用域也结束。
要创建一个比较外部 x 和 y 值的匹配表达式,而不是引入一个掩盖现有 y 变量的新变量,我们需要使用匹配保护条件。我们将在后面讨论 match guard。
多个模式
您可以使用 | 语法匹配多个模式,这是模式或操作符。
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
在上面的代码中,我们将 x 的值与匹配分支进行匹配,其中第一个有一个 or 选项,这意味着如果 x 的值与该分支中的任何一个值匹配,则该分支的代码将运行。
用 ..= 匹配值的范围
..= 语法允许我们匹配一个包含范围的值。
在下面的代码中,当模式匹配给定范围内的任何值时,该分支将执行:
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
编译器在编译时检查范围是否为空,因为 Rust 可以判断范围是否为空的类型只有 char 和数值,所以范围只允许使用这两类。
下面是一个使用 char 值范围的例子:
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
解构数据结构
我们还可以使用模式来解构结构体、枚举和元组,以使用这些值的不同部分。让我们遍历每个值。
解构结构体
假如我们有一个 Point 结构体,它有两个字段 x 和 y,我们可以使用带有 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 对匹配结构域的模式有一个简写:你只需要列出结构域的名称,从模式创建的变量将具有相同的名称。如下所示:
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);
}
这段代码创建了与 p 变量的 x 和 y 字段匹配的变量 x 和 y。结果是变量 x 和 y 包含来自 p 结构体的值。
我们还可以将字面量作为结构体模式的一部分进行解构,而不是为所有字段创建变量。这样做允许我们测试某些字段的特定值,同时创建变量来解构其他字段。
以下是一个匹配表达式,它将 Point 值分为三种情况:直接位于 x 轴上的点(当 y = 0 时为真)、位于 y 轴上的点(x = 0),或者两个轴上都没有点。
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 匹配,则 y 字段匹配。该模式仍然创建一个 x 变量,我们可以在该分支的代码中使用该变量。
类似地,第二个分支匹配 y 轴上的任何点,通过指定 x 字段匹配 0,并为 y 字段的值创建一个变量 y。
第三个分支没有指定任何字面量,因此它匹配任何其他 Point,并为 x 和 y 字段创建变量。
在本例中,由于 x 包含 0,所以值 p 与第二分支匹配,因此此代码打印 On the y axis at 7。
记住,匹配表达式一旦找到第一个匹配模式就会停止检查分支,所以即使 Point {x: 0, y: 0} 位于 x 轴和 y 轴上,这段代码也只会打印 On the x axis at 0。
解构枚举
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 color to red {r}, green {g}, and blue {b}");
}
}
}
对于没有任何数据的 enum 变量,比如 Message::Quit,我们不能再解构值了。我们只能匹配 Message::Quit 值,并且该模式中没有变量。
对于类结构的枚举变体,如 Message::Move,可以使用与指定的匹配结构的模式类似的模式。在变量名之后,我们放置花括号,然后列出包含变量的字段,这样我们就可以将各个部分分开,在代码中用于该分支。
对于类似元组的枚举变体,比如 Message::Write(包含一个元素的元组)和 Message::ChangeColor(包含三个元素的元组),其模式类似于我们指定的匹配元组的模式。模式中的变量数量必须与我们要匹配的变量中的元素数量相匹配。
解构嵌套结构体和枚举
匹配可以在嵌套项上工作。
例如,我们可以重构上一小节的代码,以便在 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}");
}
_ => (),
}
}
第一个分支的模式匹配一个包含 Color::Rgb 变体的 Message::ChangeColor 枚举变体。然后,模式绑定到三个内部 i32 值。
第二分支的模式也匹配 Message::ChangeColor 枚举变体,但是内部枚举匹配的是 Color::Hsv。
我们可以在一个匹配表达式中指定这些复杂的条件,即使涉及到两个枚举。
解构结构和元组
我们可以以更复杂的方式混合、匹配和嵌套解构模式。
下面的例子展示了一个复杂的解构,我们将结构体和元组嵌套在元组中,并将所有的原始值解构出来:
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
这段代码允许我们将复杂类型分解成它们的组成部分,这样我们就可以单独使用我们感兴趣的值。
使用模式进行解构是一种方便的方法,可以分别使用值的每个部分,例如结构体中每个字段的值。
忽略模式中的值
有时忽略模式中的值是有用的,例如在匹配的最后一部分,以获得一个实际上不做任何事情但考虑所有剩余可能值的集合。
通配符:_
我们使用下划线作为通配符模式,它将匹配任何值,但不绑定到值。这在匹配表达式的最后一部分特别有用,但是我们也可以在任何模式中使用它,包括函数参数。
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
这段代码将完全忽略作为第一个参数传递的值 3。
在大多数情况下,当你不再需要某个特定的函数参数时,你可以更改签名,使其不包括未使用的参数。在某些情况下,忽略函数参数可能特别有用,例如,当你实现一个 trait 时,你需要一个特定的类型签名,但你实现的函数体不需要其中一个参数。这样就可以避免收到关于未使用函数参数的编译器警告,而如果使用名称则会这样做。
通过嵌套 _ 忽略值的一部分
我们也可以在另一个模式中使用 _ 来忽略值的一部分,例如,当我们只想测试值的一部分,而不想使用相应代码中要运行的其他部分时。
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:?}");
在第一个匹配分支中,我们不需要匹配或使用某些变体中的值,但我们确实需要测试 setting_value 和 new_setting_value 是某些变体的情况。在这种情况下,我们输出不更改 setting_value 的原因,它不会被更改。
在第二部分中的 _ 模式表示的所有其他情况下(如果 setting_value 或 new_setting_value 中的一个为 None),我们将 new_setting_value 赋值为 new_setting_value,即 Some(10)。
我们还可以在一个模式中的多个位置使用下划线来忽略特定的值。
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
该示例忽略包含五个元素的元组中的第二个和第四个值。
以_开头的未使用变量
如果你创建了一个变量,但没有在任何地方使用它,Rust 会发出警告,因为未使用的变量可能是一个 bug。
然而,有时能够创建一个还不会使用的变量是很有用的,例如当你创建原型或刚开始一个项目时。在这种情况下,你可以告诉 Rust 不要警告未使用的变量,方法是在变量名的开头加上下划线。
fn main() {
let _x = 5;
let y = 10;
}
在这里,我们得到一个关于不使用变量 y 的警告,但是我们没有得到一个关于不使用 _x 的警告。
请注意,仅使用 _ 和使用以下划线开头的名称之间存在细微的区别。语法 _x 仍然将值绑定到变量,而 _ 根本不绑定。为了说明这种区别的重要性,请看下面两个示例。
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
我们将收到一个错误,因为 s 的值仍然会被移到 _s 中,这阻止了我们再次使用 s。
但是,使用下划线本身不会绑定到值。
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
这段代码编译后不会出现任何错误,因为 s 没有被移到 _ 中。
使用 .. 忽略部分值
对于包含许多部分的值,我们可以使用 .. 语法,以使用特定部分并忽略其余部分,从而避免为每个被忽略的值列出下划线。.. 忽略了我们在模式的其余部分中没有显式匹配的值的任何部分。
例如,我们有一个 Point 结构体,它保存三维空间中的坐标。在匹配表达式中,我们希望只对 x 坐标进行操作,而忽略 y 和 z 字段中的值。
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}"),
}
这比列出 y: _ 和 z: _ 要快得多,特别是当我们处理有很多字段的结构体时,而只有一两个字段是相关的。
.. 语法可以扩展到需要的任意多的值。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
在这段代码中,第一个和最后一个值与 first 和 last 匹配。.. 将匹配并忽略中间的所有内容。
然而,使用 .. 必须是明确的。如果不清楚哪些值需要匹配,哪些值应该忽略,Rust 会给我们一个错误。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
Rust 不可能确定在与第二个值匹配之前要忽略元组中的多少值,以及之后要忽略多少值。变量名 second 对 Rust 没有任何特殊意义,所以我们得到一个编译器错误,因为像这样在首尾两个地方使用 .. 是模棱两可的。
使用匹配守卫指定额外条件
匹配守卫(match guard)是一个附加的 if 条件,在匹配分支中的模式之后指定,它也必须与要选择的匹配分支相匹配。
匹配守卫对于表达比模式本身更复杂的想法很有用。但是请注意,它们只能在 match 表达式中使用,而不能在 if let 或 while let 表达式中使用。
条件可以使用在模式中创建的变量。下列代码显示了一个匹配,其中第一个分支具有模式 Some(x),并且还有一个匹配守卫 if x % 2 == 0(如果数字是偶数,则为真)。
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 与第一个分支中的模式比较时,它匹配,然后匹配保护检查 x 除以 2 的余数是否等于 0,因为是,所以选择第一个分支。
如果 num 改为 Some(5),则第一个分支中的匹配守卫将为假,然后 Rust 会转到第二个分支,因为第二个分支没有匹配守卫,因此会匹配任何一些变体。
在模式中没有办法表达 if x % 2 == 0 条件,因此匹配保护使我们能够表达这种逻辑。这种额外的表达性的缺点是,当涉及匹配保护表达式时,编译器不会尝试检查穷尽性。
我们之前提到可以使用匹配保护来解决模式隐藏问题。回想一下,我们在匹配表达式的模式内部创建了一个新变量,而不是在匹配之外使用该变量。这个新变量意味着我们不能针对外部变量的值进行测试。使用匹配守卫就可以解决这个问题。
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,它不会遮蔽任何东西,因为在匹配之外没有变量 n。
匹配守卫 if n == y 不是模式,因此不会引入新的变量。这个 y 是外部的 y,而不是一个新的 y,我们可以通过比较 n 和 y 来寻找一个与外部 y 相同的值。
还可以在匹配保护中使用或操作符 | 来指定多个模式,匹配守卫条件将适用于所有模式。
下面代码显示了将使用 | 的模式与匹配守卫组合在一起时的优先级。这个示例的重要部分是,if y 匹配保护适用于 4、5 和 6,尽管看起来像是 if y 只适用于 6。
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
匹配条件表明,只有当 x 的值等于 4、5 或 6并且 y 为真时,第一个分支才匹配。原因是 if 条件适用于整个模式 4 | 5 | 6,而不仅仅适用于最后一个值 6。
换句话说,与模式相关的匹配保护的优先级是这样的:
(4 | 5 | 6) if y => ...
而不是:
4 | 5 | (6 if y) => ...
@ 绑定
@ 操作符允许我们创建一个变量,该变量在我们测试该值以进行模式匹配的同时保存该值。
例如,我们想测试 Message::Hello id 字段是否在 3…=7 的范围内。我们还希望将该值绑定到变量 id_variable,以便在与分支相关的代码中使用它。
我们可以将这个变量命名为 id,与字段名称相同,但在本例中,我们将使用不同的名称。
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 的变量中可用的值。原因是我们使用了 struct 字段速记语法。但是我们没有像对前两个分支那样对这个分支中的 id 字段中的值应用任何测试:任何值都将匹配这个模式。
使用 @ 可以让我们测试一个值,并将其保存在一个模式中的变量中。