You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

24 KiB

模式和匹配

模式是一种特殊的语法,用于匹配不同类型的结构,可以是简单的也可以是复杂的。通过将模式与匹配表达式等结构相结合,可以更好地控制程序的控制流。模式可以由以下部分组成:

  • 字面量:匹配特定的常量值。
  • 解构的数组、枚举、结构或元组:从中提取值以进行匹配。
  • 变量:将匹配的部分绑定到变量,以便在代码中使用。
  • 通配符:用于匹配任意值,不关心具体内容。
  • 占位符:类似于通配符,但可以将匹配的部分绑定到特定变量。

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

match分支

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

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

例如 Option 值的 match 表达式:

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

match 表达式的一个要求是,它们必须是穷尽的,也就是说,match 表达式中值的所有可能情况都必须考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式。特定的 _ 模式将匹配任何内容,但它永远不会绑定到变量,因此通常在最后一个匹配分支中使用。例如,当您希望忽略未指定的任何值时,_ 模式可以很有用。

if let条件表达式

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

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, {}, as the background", color);
    } 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", age);
        } else{
            println!("Using blue as the background color");
        }
    }
}

image-20230830105332596

if let 也可以引入与 match 分支相同的遮蔽变量:if let Ok(age) = age 这一行引入了一个新的遮蔽的 age 变量,其中包含了 Ok 变体中的值。

覆盖变量是指在一个作用域内,一个变量的名字被重新引入并覆盖外部作用域中同名的变量。这意味着在内部作用域中,通过相同的变量名,你可以访问内部作用域中的变量,而外部作用域中的变量将被覆盖,暂时无法直接访问。

使用 if let 表达式的缺点是,编译器不会检查穷尽性,而 match 表达式会进行检查。如果省略了最后的 else 块,从而未处理某些情况,编译器将不会提示可能的逻辑错误。

while let 条件循环

只要模式匹配就一直进行 while 循环。

rustfn main() {
    let mut stack = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

image-20230830105535658

for循环

for 循环是 Rust 中最常见的循环结构。

fn main() {
    let v = vec!['a', 'b', 'c'];
    for (index, value) in v.iter().enumerate(){
        println!("{} is at index {}", value, index);
    }
}

image-20230830112314654

enumerate() 方法需要在一个迭代器上调用,因此需要首先使用 iter() 方法将对象转换为一个迭代器。enumerate() 方法会返回一个迭代器,该迭代器会生成元组,包括索引和原始值。

let语句

在本章之前,我们只明确讨论了在 matchif let 中使用模式,但实际上,我们还在其他地方使用了模式,包括在 let 语句中。

let x = 5;

let 语句更正式的写法:

let PATTERN = EXPRESSION;

像 let x = 5; 这样的语句中,PATTERN 位置上的变量名实际上是模式的一种简单的形式。Rust 会将表达式与模式进行比较,并赋予它找到的任何名称。因此,在 let x = 5; 的示例中,x 是一个模式,表示将匹配到的值绑定到变量 x。因为变量名 x 是整个模式,这个模式实际上意味着将任何值都绑定到变量 x,无论值是什么

let 和模式解构一个元组:

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

这里将一个元组与模式匹配。Rust 会比较值 (1, 2, 3) 与模式 (x, y, z) 并发现此值匹配这个模式。可以将这个元组模式看作是将三个独立的变量模式结合在一起。

当模式中元素数量与元祖元素不匹配时:

  1. 当元素值多于模式值:

    1. 多出一个时:_ 通常用作通配符,表示忽略掉对应位置的值。

      let (x, _, y) = (1, 2, 3);
      
      // x = 1, y = 3
      
    2. 多出大于一个时:.. 用于表示忽略某个范围内的元素。

      let (x, y, ..) = (1, 2, 3, 4, 5);
      
  2. 当元素值少于模式值:去除模式中多出的变量.

函数参数

函数参数也可以是模式。

fn foo(x: i32) {
    // 代码在这里
}

这里的 x 就是一个模式,故可以像在let时所做的一样,我们也可以在函数的参数中匹配一个元组到模式:

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

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

image-20230830150622308

因为闭包与函数相似,故可以在闭包参数列表中以与函数参数列表相同的方式使用模式。

Refutability:何时模式可能会无法匹配

模式有两种形式:可反驳(refutable)不可反驳(irrefutable)。对于任何可能传递的值都会匹配的模式是不可反驳的。例如:let x = 5; 这个模式中的 x 可以匹配任何值,因此不会失败。

当模式可能无法与某些可能的值匹配时,这个模式被称为可反驳的模式。一个示例是表达式 if let Some(x) = a_value 中的 Some(x),因为如果 a_value 变量中的值是 None 而不是 Some,Some(x) 模式将无法匹配。

让我们看一个示例,当我们尝试在 Rust 要求不可反驳模式的地方使用可反驳模式

let value: Option<i32> = Some(1);
// let value: Option<i32> = None;
let Some(s) = value;  //  ^^^^^^^ pattern `None` not covered

如果 some_option_value 是 None 值,它将无法匹配模式 Some(x),这意味着模式是可反驳的。然而,let 语句只能接受不可反驳的模式。

error[E0005]: refutable pattern in local binding
 --> src\main.rs:3:9
  |
3 |     let Some(s) = value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
  = note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
  |
3 |     let Some(s) = value else { todo!() };
  |                         ++++++++++++++++

error[E0005]: refutable pattern in local binding

修改代码通过 if let 解决此问题:

fn main() {
    let value: Option<i32> = Some(1);
    if let Some(x) = value {
        println!("x: {0}", x);
    }

    let value: Option<i32> = None;
    if let Some(x) = value {
        println!("x: {0}", x);
    }
}

image-20230830153445776

if let 提供了一个总是会匹配的模式:

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

代码可运行,但编译器会发出警告:

warning: irrefutable `if let` pattern
  --> src\main.rs:11:8
   |
11 |     if let x = 5 {
   |        ^^^^^^^^^
   |
   = note: this pattern will always match, so the `if let` is useless
   = help: consider replacing the `if let` with a `let`
   = note: `#[warn(irrefutable_let_patterns)]` on by default

在 Rust 中,模式分为不可反驳模式和可反驳模式:

不可反驳模式(Irrefutable Patterns): 不可反驳的模式确保总是会成功匹配,并且总是有一个对应的值。

  • 用于确定位置的情况,不会导致匹配失败。
  • 例如:变量绑定函数参数for 循环中的迭代器解构等。
  • 适用于所有可能的值,不会引起匹配失败。

**可反驳模式(Refutable Patterns):**可反驳模式是指在模式匹配时可能会失败的模式。

  • 用于可能导致匹配失败的情况。
  • 例如:match 表达式、if let 表达式、while let 表达式,以及解构复合类型(如元组、结构体、枚举)。
  • 需要通过条件语句(if letwhile let)或 match 表达式来处理可能的匹配失败情况。

需要注意的是,编写稳健的 Rust 代码需要正确处理模式匹配失败的情况,以确保程序的可靠性。不可反驳的模式用于确定位置,不会导致匹配失败,而可反驳的模式用于处理可能的匹配失败情况。

模式语法

匹配字面量

fn main() {
    let x = 1;
    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}
// one

匹配变量名

命名变量是匹配任何值的不可反驳模式,match表达式可能会出现变量覆盖(shadowing)问题。

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 = {:?}, y = {:?}", x, y);
}
// Matched, y = 5
// at the end: x = Some(5), y = 10

首先,代码会尝试匹配模式Some(50),但是x的值是Some(5),因此不匹配这个分支。接着,代码会尝试匹配模式Some(y),在这个模式中,匹配会创建一个新的变量y,这个新的 y 绑定会匹配任何 Some 中的值。然后代码会进入这个分支,输出Matched, y = 5。最后,_分支是一个通配分支,如果前面的模式都不匹配(即 x 为None时),就会进入这个分支,输出Default case, x = None

match 表达式完成时,它的作用域结束,内部的 y 作用域也随之结束。最后的 println! 语句会输出 at the end: x = Some(5), y = 10

多模式匹配

match 表达式中,可以使用 | 语法来匹配多个模式,这就是模式或运算符。

x = 1;

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

..= 匹配值的范围

只允许在 数字char 上使用范围。

let x = 5;

match x {
    1 ..= 5 => println!("one through five"),
    _ => println!("something else"),
}
// one through five 

在计算机内部,字符类型实际上是用数字表示的。在 ASCII 编码中,字母 'a' 对应的整数值是 97,字母 'b' 对应的整数值是 98,依此类推。

let x = 'c';

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

解构并分解值

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

解构结构体

如果你只想要结构体中的一个字段,而不关心另一个字段,你可以使用下划线 _ 来忽略那个不需要的字段。

struct Point {
    x: i32,
    y: i64,
}

fn main() {
    let p = Point {x: 0, y: 7};
    let Point {x: a, y:b } = p;
    println!("a is {}, b is {}", a, b);
    // assert_eq!(0, a); assert_eq!(7, b);
}

image-20230831135203062

此代码创建了变量 ab,它们匹配了 p 结构体的 xy 字段的值。在这里变量名与结构体字段名不一致,可进行如下修改:

struct Point {
    x: i32,
    y: i32,
}

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

image-20230831135943745

这段代码创建了与变量 pxy 字段匹配的变量 xy。结果是变量 xy 包含了来自结构体 p 的值。

下面使用 match 表达式,处理不同三种情况的Point值:

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})");
        }
    }
}
// On the y axis at 7

image-20230831143943913

在 Rust 中,模式匹配可以创建变量,但这些变量的作用域通常只在匹配的代码块内。一旦离开该代码块,这些变量就会被销毁。

解构枚举

下面以第六章的 Message 枚举为例,编写了一个 match 语句,使用相应的解构模式,对每个内部值进行解构:

enum Message {
    Quit,
    Move {x:i32, y:i32},
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 225);

    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 225

解构嵌套的结构体和枚举

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

image-20230831154655279

解构结构体和元组

我们可以以更复杂的方式合成、匹配和嵌套解构模式。我们在一个元组中嵌套了结构体和元组,并将所有的原始值解构出来:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
    println!("{}, {}, {}, {}", feet, inches, x, y);
}
// 3, 10, 3, -10

解构引用

解构引用通常用于解构结构体、元组或其他自定义类型的引用,以便访问其字段或元素。

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let point = Point { x: 5, y: 10 };
    let reference = &point;

    match reference {
        &Point { x, y } => {
            println!("x: {}, y: {}", x, y);
        }
    }
}
// x: 5, y: 10

image-20230901150749764

忽略模式中的值

使用_来忽略整个值

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

fn main() {
    foo(3, 4);
}

使用嵌套的_忽略部分值

下面场景为:用户不应该被允许覆盖现有设置的自定义值。故并不在乎值是什么:

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

image-20230831163512803

仅当 setting_value 为 None时,才可以赋值。当 setting_valuenew_setting_value 都为实际值时会打印 Can't overwrite an existing customized valuenew_setting_value 为 None 会重置 setting_value

image-20230831162408620

我们还可以在一个模式中的多个位置使用下划线来忽略特定值。

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

match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {}, {}, {}", first, third, fifth);
    }
}
// Some numbers: 2, 8, 32

通过在名字前以一个下划线开头来忽略未使用的变量

fn main() {
    let _x = 5;
    let y = 10;
}

这时只会产生一个警告

warning: unused variable: `y`
 --> src\main.rs:3:9
  |
3 |     let y = 10;
  |         ^ help: if this is intentional, prefix it with an underscore: `_y`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `ignore` (bin "ignore") generated 1 warning (run `cargo fix --bin "ignore"` to apply 1 suggestion)

  1. 下划线 _
    • 使用单独的下划线 _ 作为变量名,它表示一个未使用的变量或占位符变量,并且不会将值绑定到该变量。它在模式匹配中用于表示不关心的情况,以及避免未使用变量的警告。
    • 示例:let _ = 5; 表示创建一个未使用的变量,但不会绑定任何值。
  2. 下划线开头的变量 _x
    • 使用 _x 这样以下划线开头的变量名,它仍然是一个有效的变量名,并且会将值绑定到该变量。但在命名惯例中,以 _ 开头的变量名通常用于表示未使用的变量或占位符。这使得 _x 的用途和含义更明确。
    • 示例:let _x = 5; 表示创建一个变量 _x,并将值 5 绑定到该变量。
fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    // println!("{:?}", s);   //  ^ value borrowed here after partial move
}
fn main() {
    let s = Some(String::from("Hello!"));
    
    if let Some(_) = s {
        println!("found a string");
    }
    
    println!("{:?}", s);
}
// found a string
// Some("Hello!")

用 .. 忽略剩余值

结构体中不能使用 .. 跳过前面的字段,需要显式列出每个字段的模式匹配。而元组可以使用 .. 语法来跳过前面的元素。若要取结构体第二个值 y,也需要显示写出前面的字段 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),
    // Point { x, y ,..} => println!("x is {}", y),
}

image-20230831172553422

当仅需要元组的第一个和最后一个值时:

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

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

image-20230831172050798

使用 .. 必须是无歧义的

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

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

变量名second对 Rust 来说没有任何特殊意义,若运行此代码会产生错误:

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

匹配守卫(Match Guards)和额外条件

匹配守卫是在模式匹配的分支中额外添加的条件判断。它允许我们对模式进行更复杂的判断,以选择特定的分支。匹配守卫可以使用分支中定义的变量,并且可以用于处理无法直接在模式中表达的条件逻辑。

fn main() {
    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

使用匹配守卫来解决模式遮蔽问题:

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)
// at the end: x = Some(5), y = 10

// 当 let x = Some(10); : 
// Matched, n = 10
// at the end: x = Some(10), y = 10

匹配守卫中的 if n == y 并不是一个模式,因此不会引入新变量。这里的 y 是外部的 y,而不是一个新的遮蔽 y,我们可以通过比较 n(x 中的值) 和 y 来查找具有与外部 y 相同值的值。

在匹配守卫中可以使用逻辑或运算符 | 来指定多个模式;匹配守卫的条件将适用于所有这些模式。

fn main() {
    let x = 5;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}
// no

if条件适用于整个模式 4 | 5 | 6,而不仅仅是最后的值 6。

@ 绑定

@ 操作符允许我们同时在测试值是否匹配模式的同时创建一个变量来持有这个值。

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

image-20230901144604952

第一个分支通过在范围 3..=7 前指定 id_variable @,捕获了任何匹配此范围的值,同时将其打印了出来。在第二个分支中模式代码无法使用 id 字段中的值,因为我们没有将 id 值保存在变量中。在最后一个分支中,任何值都将与此模式匹配。

使用 @ 允许我们在一个模式中测试一个值并将其保存在一个变量中。