写点什么

Rust 从 0 到 1- 模式 - 相关语法

用户头像
关注
发布于: 2 小时前
Rust从0到1-模式-相关语法

在此之前我们已经看到过很多不同的模式的例子。在本节中,我们将展示所有模式相关的语法,并讨论其使用场景。

匹配常量值

我们可以直接与特定的值进行匹配。参考下面的例子:

let x = 1;
match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"),}
复制代码

上面的例子会打印出 one ,因为 x 的值是 1。这适用于我们希望代码在碰到某个具体的值的时候执行一些操作。

匹配有名变量

有名变量(named variables)是一种不可反驳模式,并且可以匹配任何值。当在 match 表达式中使用的时候它的时候,有些地方需要注意:因为 match 会创建一个新的作用域,在 match 表达式中作为模式的一部分声明的变量会覆盖其外部的同名变量,就像我们在新作用域内定义的变量一样。参考下面的例子:

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 = {:?}, y = {:?}", x, y);
复制代码

在上面的例子中,我们声明了变量 x 和 y。接着创建了一个 match 表达式:第一个匹配分支的模式 Some(50) 与 x 并不匹配,代码继续执行;第二个匹配分支中的模式引入了一个变量 y,它会匹配任何 Some 中的值,因为 match 表达式会创建新的作用域,所以这是一个新变量,而不是在开始声明的那个 y。这个新的变量 y 会匹配任何 Some 中的值并与之绑定,在这里是 5。因此这个分支的表达式将会执行并打印出 Matched, y = 5。

如果 x 的值是 None,则前面两个分支的模式都不会匹配,最终会匹配默认的 _ 。由于在这个分支的模式中并没有引入新的名称为 x 的变量,所以此时表达式中的 x 就是外部我们开始定义的 x。这时将会打印 Default case, x = None。

当 match 表达式执行完以后,其作用域也就结束了,其内部 y 的作用域也结束了。因此,最后的 println! 会打印出 at the end: x = Some(5), y = 10,这里的变量 y 是开始我们定义的 y。

为了在 match 匹配分支中够使用外部 x 和 y 的值,而不被覆盖,我们需要使用 “match 卫语句”(match guard),我们将在稍后的小节讨论它。

匹配多个模式

在 match 表达式中,可以使用 | 语法匹配多个模式,它代表“或”的意思。参考下面的例子:

let x = 1;
match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"),}
复制代码

在上面的例子中,第一个分支代表 x 匹配 1 或 2 任意一个模式就可以,由于 x 的值是 1,这段代码会打印出 one or two。

使用 ..= 匹配某个范围内的值

语法 ..= 允许我们匹配一个闭区间范围内的值。参考下面的例子:

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..=5 指定范围要简短的多,特别是在范围更大的时候,譬如 1 到 1000 ;)。

这个语法只能用于数字或字符类型,因为编译器会在编译时检查范围是否为空,而在 Rust 中只有数字和字符类型是可以判断范围是否为空的类型。下面我们看看字符范围的例子:

let x = 'c';
match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"),}
复制代码

上面的例子中,这段代码会打印出 early ASCII letter。

解构数据

我们还可以使用模式来解构结构体、枚举、元组和引用,以便可以使用这些值中的不同部分。下面让我们看看每一个的用法。

解构结构体

我们可以使用 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);}
复制代码

上面的代码中,let Point { x, y } = p; 创建了变量 x 和 y,与结构体 p 中的字段名相同,即变量 x 和 y 的值等于结构体 p 中字段 x 和 y 的值。

我们也可以使用常量值(literal values)作为结构体模式的一部分,这样我们可以在其中一些字段为特定值的时候,才将其它字段的值赋给相应的变量。参考下面的例子:

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), }}
复制代码

在上面的例子中,match 表达式将 Point 分成了三种场景:位于 x 轴上(y = 0)、位于 y 轴上(x = 0)以及既不在 x 轴上也不在 y 轴上。第一个分支通过指定字段 y 匹配常量 0 来匹配 x 轴上的点,同时还创建了变量 x 以在分支的代码中使用。与之类似,第二个分支通过指定字段 x 匹配常量 0 来匹配 y 轴上的点,并创建了变量 y。第三个分支使用前面介绍的简写的方式匹配其它的 Point 。上面这段代码最终会打印出 On the y axis at 7。

解构枚举

在前面介绍 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 {} and in the y direction {}", x, y ); } Message::Write(text) => println!("Text message: {}", text), Message::ChangeColor(r, g, b) => println!( "Change the color to red {}, green {}, and blue {}", r, g, b ), }}
复制代码

上面这段代码会打印出 Change the color to red 0, green 160, and blue 255。

对于不包含任何数据的枚举成员 Message::Quit,无法对其进行解构。只能直接与 Message::Quit 进行匹配,模式中也不包含任何变量。

对于类似结构体类型的成员 Message::Move ,可以使用和匹配结构体类似的模式进行结构,同样也可以使用简写的方式。

对于类似元组类型的成员,包含一个元素的 Message::Write 和包含三个元素的 Message::ChangeColor,可以使用和匹配元组类似的模式进行结构,不过需要注意模式中变量的数量必须与成员所包含的元素数量一致。

大家试着改变 msg 的值来执行其他分支代码。

解构互相嵌套的结构体和枚举

截至目前,我们所有的例子都只是对深度为一的结构体或枚举进行匹配。Rust 所能做到的不止如此,参考下面的例子:

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 the color to red {}, green {}, and blue {}", r, g, b ), Message::ChangeColor(Color::Hsv(h, s, v)) => println!( "Change the color to hue {}, saturation {}, and value {}", h, s, v ), _ => (), }}
复制代码

上面的例子中我们重构了枚举类型 Message ,以同时支持 RGB 和 HSV 色彩模式。match 表达式第一个分支的模式匹配一个包含枚举成员 Color::Rgb 的枚举成员 Message::ChangeColor;第二个分支的模式匹配一个包含枚举成员 Color::Hsv 的枚举成员 Message::ChangeColor。这是一个深度为二的嵌套,如果我们愿意,可以继续嵌套下去。

解构结构体和元组

我们可以使用更复杂的混合、匹配和嵌套的模式进行解构。参考下面的例子:

let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
复制代码

在上面的例子中,我们对一个包含结构体和元组的元组进行解构。这将复杂的类型分解成部分以便可我们以单独使用我们感兴趣的值。通过模式解构我们可以方便的使用复杂类型中的每部分值,譬如结构体中每个字段的值。

在模式中忽略某些值

我们已经知道使用忽略值的模式在有些场景很有用,譬如 match 的最后一个分支,让我们可以方便的处理所有其它我们可能太关心的情况。在模式中有一些方法可以忽略全部或部分值:使用 _ ,将 _ 嵌套在其它模式中使用,使用一个以下划线开始的名称,或者使用 .. 忽略部分值。下面让我们逐一讨论。

使用 _ 忽略全部值

我们已经使用过下划线 _ 作为通配符,可以匹配任何值但并不与之绑定。虽然模式 _  作为 match 表达式最后的分支非常有用,但是其也可以用于其它场景中,譬如函数参数:

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 时,我们需要符合其定义,但是函数实现并不需要某个参数时,这样编译器就不会警告说存在未使用的函数参数。

嵌套使用 _ 忽略部分值

我们也可以在一个模式内使用 _ 忽略部分值,例如,当我们只需要测试值的某些部分,对于其它部分不关心或没有使用到时。假设以下业务需求:用户不允许覆盖已有的设置,但是可以删除设置,也可以在未设置时为其设置。参考下面的代码实现:

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) 两段。在 match 的第一个分支中,我们不关心 Some 成员中具体的值,仅关心其是否 Some 成员。我们只是打印出提示信息,而不需要对 Some 成员做任何操作。

对于剩下的其它场景(setting_value 或 new_setting_value 任一为 None 或全部为 None),即在使用 _ 最后的分支中,我们将 new_setting_value 赋值给 setting_value。

我们还可以在一个模式中的多处使用下划线来忽略某些特定的值,参考下面的例子:

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 不要警告,这时可以用下划线开头命名变量。参考下面的例子:

fn main() {    let _x = 5;    let y = 10;}
复制代码

上面的例子中创建了两个未使用变量,当编译代码时只会得到其中一个的警告,即未使用变量 y。

另外需要注意, 只使用 _ 和使用以 _ 开头的变量名是不同的:_x 会将值绑定到变量,而 _ 则不会。参考下面的例子:

let s = Some(String::from("Hello!"));
if let Some(_s) = s { println!("found a string");}
println!("{:?}", s);
复制代码

如果尝试编译上面的代码我们将会得到错误,因为 s 的值会移动到 _s,之后我们不能再使用 s。但是只使用 _,则不会产生错误,因为 s 的值不会被移动。参考下面的例子:

let s = Some(String::from("Hello!"));
if let Some(_) = s { println!("found a string");}
println!("{:?}", s);
复制代码
使用 .. 忽略值剩下的部分

对于包含有多个部分的值,我们可以只是用其中一部分并使用语法 .. 忽略其它部分,也避免了对于每个忽略的部分使用 _ 。.. 会忽略值中与模式未匹配的剩余部分。参考下面得例子:

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),}
复制代码

在上面的例子中,Point 结构体用于存放三维空间的坐标,而在 match 表达式中,我们希望只关心 x 坐标并忽略了剩下的 y 和 z 坐标。在例子中我们使用了 .. ,这比列出 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) }, }}
复制代码

尝试编译上面的代码,我们会得到类似下面的错误:

$ 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: aborting due to previous error
error: could not compile `patterns`
To learn more, run the command again with --verbose.
复制代码

Rust 无法决定在匹配 second 之前和之后应该忽略元组中的多少个元素。可能是忽略之前的 2 和之后的 8、16 、32;也可能是忽略之前的 2、4 和之后的 16 、32,等等。变量名 second 对于 Rust 来说并没有任何意义,因此在这里使用 .. 是有歧义的,Rust 会在编译时报错。

使用 match 卫语句增加额外的条件

match 卫语句(match guard)是位于 match 分支中模式之后的额外 if 条件,需要同时符合,分支才会执行。通过它可以表达比单独的模式更为复杂的场景。在这个 if 条件中可以使用模式中创建的变量。参考下面的例子:

let num = Some(4);
match num { Some(x) if x < 5 => println!("less than five: {}", x), Some(x) => println!("{}", x), None => (),}
复制代码

上例中的这段代码会打印出 less than five: 4。当变量 num 与 match 第一个分支中的模式比较时,Some(4) 与模式 Some(x) 匹配;然后 match 卫语句检查 x 值是否小于 5,而 x 值是 4 小于 5,所以第一个分支被执行。如果 num 为 Some(10),因为 10 大于 5  ,match 卫语句检查不通过,则 Rust 会继续尝试与第二个分支匹配。

我们无法在模式中表达类似 if x < 5 的匹配条件,因此 Rust 提供了 match 卫语句以获得表达这种逻辑的能力。

在前面我们提到可以使用 match 卫语句来解决模式中变量覆盖的问题,在 match 表达式的分支中,模式会新建一个变量而不是使用外部的变量,这意味着无法与外部变量的值进行匹配。我们可以使用 match 卫语句解决这个问题,参考下面的例子:

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 = {:?}, y = {}", x, y);}
复制代码

上例这段代码会打印出 Default case, x = Some(5)。在 match 的第二个分支中卫语句 if n == y 并不是一个模式,所以创建新的变量,这里的 y 就是外部定义的变量 y,这样我们就可以通过比较 n 和 y 的值是否相等来表达寻找一个与外部 y 相同的值的匹配逻辑了。

另外需要注意的是,在多模式匹配的时候,卫语句的条件是作用于所有的模式。参考下面的例子:

let x = 4;let y = false;
match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"),}
复制代码

在上面的例子中卫语句 if y 作用于模式 4、5 和 6,即使看起来好像其只作用于 6。这个分支代表当 x 的值为 4、5 或 6 之一,同时 y 为 true 的场景。因此,上面这段代码会打印出 no。卫语句与模式的优先级关系类似下面这样:

(4 | 5 | 6) if y => ...
复制代码

而不是:

4 | 5 | (6 if y) => ...
复制代码

通过运行代码的结果我们也可以证明这一点:如果卫语句只作用于 | 运算符的最后一个模式,那么结果应该是打印出 yes。

@ 绑定

@ 运算符允许我们在创建一个用于存放值的变量,并检查其是否与模式匹配。参考下面的例子:

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),}
复制代码

上例中我们在第一个分支中测试 Message::Hello 的 id 字段是否位于 3..=7 范围内,同时将其值绑定到变量 id_variable,以便此在此分支相关的代码中可以使用它。第二个分支只指定了一个范围模式,并没有一个包含 id 字段实际值的变量,因此在分支相关的代码中无法使用 id 字段中的值。最后一个分支是匹配结构体字段的简写,这个在之前已经介绍过。这段代码会打印出 Found an id in range: 5。

使用 @ 让我们可以在一个模式中测试指定的值并保存到变量中。

总结

Rust 中的模式非常有用,它帮助我们区分不同类型的数据。当在 match 语句中使用时,Rust 会确保模式会覆盖每一个可能的值,否则将无法编译通过。let 语句和函数参数中的模式使我们可以将值解构,同时赋值给变量。我们可以根据自己的需要创建简单或复杂的模式来满足各种场景。

接下来,我们将介绍一些 Rust 的高级特性。

发布于: 2 小时前阅读数: 6
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-模式-相关语法