Rust 模式与模式匹配

原文基础上有删减,原文链接 模式与模式匹配

模式(Patterns)是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。模式由如下一些内容组合而成:

  • 字面值
  • 解构的数组、枚举、结构体或者元组
  • 变量
  • 通配符
  • 占位符

所有可能会用到模式的位置

match 分支

一个模式常用的位置是 match 表达式的分支。在形式上 match 表达式由 match 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式:

//match 关键字、用于匹配的值、一个或多个分支
match VALUE {
    //分支包含一个模式和在值匹配分支的模式时运行的表达式
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

匹配变量 x 中 Option<i32> 值的 match 表达式:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

match 表达式必须是 穷尽(exhaustive)的,意为 match 表达式所有可能的值都必须被考虑到,一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式

注:有一个特定的模式 _ 可以匹配所有情况,不过它从不绑定任何变量。

if let 条件表达式

if let 表达式主要用于编写等同于只关心一个情况的 match 语句简写的,if let 可以对应一个可选的带有代码的 else 在 if let 中的模式不匹配时运行。

结合 if let、else if、else if let 以及 else,示例针对不同条件的检查来决定背景颜色应该是什么:

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {  //像 match 分支那样引入覆盖变量
        if age > 30 {              //条件需要位于这个代码块内部
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
    //打印出 Using purple as the background color
}

if let 表达式的缺点在于其穷尽性没有为编译器所检查,而 match 表达式则检查了。

while let 条件循环

while let 条件循环允许只要模式匹配就一直进行 while 循环。示例使用 vector 作为栈并以先进后出的方式打印出 vector 中的值:

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{}", top);
}
//打印出 3、2 、1

for 循环

在 for 循环中,模式是 for 关键字直接跟随的值,正如 for x in y 中的 x。使用 for 循环来解构或拆开一个元组作为 for 循环的一部分:

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}
//打印结果
//a is at index 0
//b is at index 1
//c is at index 2

let 语句

每一次像这样使用 let 语句就是在使用模式:

let x = 5;

let 语句更为正式的样子如下:

let PATTERN = EXPRESSION;

因为名称 x 是整个模式,这个模式实际上等于 “将任何值绑定到变量 x,不管值是什么”。

使用 let 和模式解构一个元组:

let (x, y, z) = (1, 2, 3);

如果模式中元素的数量不匹配元组中元素的数量,则整个类型不匹配,并会得到一个编译时错误:

//为了修复错误,可以使用 _ 或 .. 来忽略元组中一个或多个值
let (x, y) = (1, 2, 3);

函数参数

函数参数也可以是模式,在参数中使用模式的函数签名:

fn foo(x: i32) {
    // code goes here
}

一个在参数中解构元组的函数:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}
//会打印出 Current location: (3, 5)

Refutability(可反驳性): 模式是否会匹配失效

模式有两种形式:refutable(可反驳的)irrefutable(不可反驳的)

  • 能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable),如 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败。
  • 对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable),如 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。

函数参数、let 语句和 for 循环只能接受不可反驳的模式,因为当值不匹配时,程序无法进行有意义的操作。if let 和 while let 表达式可以接受可反驳和不可反驳的模式,但编译器会对不可反驳的模式发出警告,因为根据定义它们旨在处理可能的失败:条件表达式的功能在于它能够根据成功或失败来执行不同的操作。

尝试在 let 中使用可反驳模式,这不能编译:

//let 对于 None 匹配不能产生任何合法的代码
let Some(x) = some_option_value;

使用 if let 和一个带有可反驳模式的代码块来代替 let,修复上面的错误:

if let Some(x) = some_option_value {
    println!("{}", x);
}

如果为 if let 提供了一个总是会匹配的模式,编译器会给出一个警告:

if let x = 5 {
    println!("{}", x);
};

match匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式。Rust 允许我们在只有一个匹配分支的match中使用不可反驳模式,不过这么做不是特别有用,并可以被更简单的 let 语句替代。

所有的模式语法

匹配字面值

直接匹配字面值模式:

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}
//会打印 one 因为 x 的值是 1

匹配命名变量

命名变量是匹配任何值的不可反驳模式,然而当其用于 match 表达式时情况会有些复杂。因为 match 会开始一个新作用域,match 表达式中作为模式的一部分声明的变量会覆盖 match 结构之外的同名变量,与所有变量一样。

一个 match 语句其中一个分支引入了覆盖变量 y:

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 = {y}", x);
//打印结果 Matched, y = 5

多个模式

在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或(or)运算符模式。
第一个分支有 选项,意味着如果 x 的值匹配此分支的任一个值就会运行:

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}
// 打印结果 one or two

通过 ..= 匹配值的范围

..= 语法允许匹配一个闭区间范围内的值,当模式匹配任何在给定范围内的值时该分支会执行:

let x = 5;

//如果 x 是 1、2、3、4 或 5,第一个分支就会匹配
match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

编译器会在编译时检查范围不为空,而 char 和数字值是 Rust 仅有的可以判断范围是否为空的类型,所以范围只允许用于数字或 char 值

一个使用 char 类型值范围的例子:

let x = 'c';

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}
//会打印出 early ASCII letter

解构并分解值

也可以使用模式来解构结构体、枚举和元组,以便使用这些值的不同部分。

解构结构体

带有两个字段 x 和 y 的结构体 Point,可以通过带有模式的 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);
}

对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。使用结构体字段简写来解构结构体字段:

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);
}

也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。解构和匹配模式中的字面值:

fn main() {
    let p = Point { x: 0, y: 7 };

    //将 Point 值分成了三种情况:
    //直接位于 x 轴上(此时 y = 0 为真)
    //位于 y 轴上(x = 0)
    //不在任何轴上的点
    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})");
        }
    }
}
//会打印出 On the y axis at 7

记住 match 表达式一旦找到一个匹配的模式就会停止检查其它分支,所以即使 Point { x: 0, y: 0} 在 x 轴上也在 y 轴上也只会打印 On the x axis at 0。

解构枚举

解构包含不同类型值成员的枚举,编写一个 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 {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

解构嵌套的结构体和枚举

所有的例子都只匹配了深度为一级的结构体或枚举,不过当然也可以匹配嵌套的项:

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}")
        }
        _ => (),
    }
}

解构结构体和元组

可以用复杂的方式来混合、匹配和嵌套解构模式。如结构体和元组嵌套在元组中,并将所有的原始类型解构出来:

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);
}
//会打印出 This code only uses the y parameter: 4

在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时需要特定类型签名但是函数实现并不需要某个参数时,这样可以避免一个存在未使用的函数参数的编译警告

使用嵌套的 _ 忽略部分值

可以在一个模式内部使用_ 忽略部分值,当只需要测试部分值但在期望运行的代码中没有用到其他部分时。
当不需要 Some 中的值时在模式内使用下划线来匹配 Some 成员:

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)

也可以在一个模式中的多处使用下划线来忽略特定值:

let numbers = (2, 4, 8, 16, 32);

//忽略了一个五元元组中的第二和第四个值
match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {first}, {third}, {fifth}")
    }
}
//会打印出 Some numbers: 2, 8, 32

用 .. 忽略剩余值

对于有多个部分的值,可以使用 .. 语法来只使用特定部分并忽略其它值,同时避免不得不每一个忽略值列出下划线
通过使用 .. 来忽略 Point 中除 x 以外的字段:

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),
}

用 first 和 last 来匹配第一个和最后一个值,.. 将匹配并忽略中间的所有值:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

使用 .. 必须是无歧义的,否则 Rust 会报错,一个带有歧义的 .. 例子:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}

变量名 second 对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 .. 是有歧义的。

匹配守卫提供的额外条件

匹配守卫(match guard)是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。

在模式中加入匹配守卫:

let num = Some(4);

match num {
    Some(x) if x % 2 == 0 => println!("The number {} is even", x),
    Some(x) => println!("The number {} is odd", x),
    None => (),
}
//会打印出 The number 4 is even

无法在模式中表达类似 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 = {:?}, y = {y}", x);
}
//会打印出 Default case, x = Some(5)

可以在匹配守卫中使用 或 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式:

let x = 4;
let y = false;

match x {
    //此分支值匹配 x 值为 4、5 或 6 同时 y 为 true 的情况
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

匹配守卫与模式的优先级关系看起来像这样:

(4 | 5 | 6) if y => ...

而不是:

4 | 5 | (6 if y) => ...

@ 绑定

at 运算符(@)允许在创建一个存放值的变量的同时测试其值是否匹配模式。使用 @ 在模式中绑定值的同时测试它:

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

热门相关:黑暗血时代   三国之袁氏枭雄   黑暗血时代   貌似纯洁   万古第一帝