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.

11 KiB

match和if let

在Rust中模式匹配最常用的就是matchif let,本章节将对两者及相关的概念进行详尽介绍。

先来看一个关于match的简单例子:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}

这里我们想去匹配dire对应的枚举类型因此在match中用三个匹配分支来完全覆盖枚举变量Direction的所有成员类型,有以下几点值得注意:

  • match的匹配必须要穷举出所有可能,因此这里用_来代表未列出的所有可能性
  • match的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X | Y,是逻辑运算符,代表该分支可以匹配X也可以匹配Y,只要满足一个即可

其实match跟其他语言中的switch非常像,_类似于switch中的default

match匹配

首先来看看match的通用形式:

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2;
    },
    _ => 表达式3
}

该形式清晰的说明了何为模式,何为模式匹配:将模式与target进行匹配,即为模式匹配,而模式匹配不仅仅局限于match,后面我们会详细阐述。

match允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行对应的代码,下面让我们来一一详解,先看一个例子:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny =>  {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

value_in_cents函数根据匹配到的硬币类似,返回对应的美分数值,match后紧跟着的是一个表达式,跟if很像,但是if后的表达式必须是一个布尔值,而match后的表达式返回值可以是任意类型,只要能跟后面的分支中的模式匹配起来即可,这里的coin是枚举Coin类型。

接下来是match的分支。一个分支有两个部分:一个模式和针对该模式的处理代码。第一个分支的模式是Coin::Penny,其后的=>运算符将模式和将要运行的代码分开。这里的代码就仅仅是表达式1,不同分支之间使用逗号分隔。

match表达式执行时,它将目标值coin按顺序依次与每一个分支的模式相比较,如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。

每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。如果分支有多行代码,那么需要用{}包裹,同时最后一行代码需要是一个表达式。

使用match表达式赋值

还有一点很重要,match本身也是一个表达式,因此可以用它来赋值:

enum IpAddr {
   Ipv4,
   Ipv6
}

fn main() {
    // let d_panic = Direction::South;
    let ip1 = IpAddr::Ipv6;
    let ip_str = match ip1 {
        IpAddr::Ipv4 => "127.0.0.1",
        _ => "::1",
    };

    println!("{}", ip_str);
}

因为这里匹配到_分支,所以将"::1"赋值给了ip_str.

模式绑定

模式匹配的另外一个重要功能是从模式中取出绑定的值,例如:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState), // 25美分硬币
}

其中Coin::Quarter成员还存放了一个值美国的某个州因为在1999年到2008年间美国在25美分(Quarter)硬币的背后为50个州印刷了不同的标记其它硬币都没有这样的设计

接下来我们希望在模式匹配中获取到25美分硬币上刻印的州的名称

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => { 
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

上面代码中,在匹配Coin::Quarter(state)模式时,我们把它内部存储的值绑定到了state变量上,因此state变量就是对应的UsState枚举类型。

例如有一个印了阿拉斯加州标记的25分硬币Coin::Quarter(UsState::Alaska)), 它在匹配时,state变量将被绑定UsState::Alaska的枚举值。

再来看一个更复杂的例子:

enum Action {
    Say(String),
    MoveTo(i32, i32),
    ChangeColorRGB(u16, u16, u16),
}

fn main() {
    let actions = [ 
        Action::Say("Hello Rust".to_string()),
        Action::MoveTo(1,2),
        Action::ChangeColorRGB(255,255,0),
    ];
    for action in actions {
        match action {
            Action::Say(s) => {
                println!("{}", s);
            },
            Action::MoveTo(x, y) => {
                println!("point from (0, 0) move to ({}, {})", x, y);
            },
            Action::ChangeColorRGB(r, g, _) => {
                println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
                    r, g,
                );
            }
        }
    }
}

运行后输出:

$ cargo run
   Compiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/world_hello`
Hello Rust
point from (0, 0) move to (1, 2)
change color into '(r:255, g:255, b:0)', 'b' has been ignored

穷尽匹配

在文章的开头,我们简单总结过match的匹配必须穷尽所有情况,下面来举例说明,例如:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
    };
}

我们没有处理Direction::West的情况,因此会报错:

error[E0004]: non-exhaustive patterns: `West` not covered // 非穷尽匹配,`West`没有被覆盖
  --> src/main.rs:10:11
   |
1  | / enum Direction {
2  | |     East,
3  | |     West,
   | |     ---- not covered
4  | |     North,
5  | |     South,
6  | | }
   | |_- `Direction` defined here
...
10 |       match dire {
   |             ^^^^ pattern `West` not covered // 模式`West`没有被覆盖
   |
   = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
   = note: the matched value is of type `Direction`

首先,不禁想感叹,Rust的编译器真**强大忍不住爆粗口了sorry如果你以后进一步深入使用Rust也会像我这样感叹的。

其次Rust知道match中没有覆盖的具体分支,知道哪些模式没有被覆盖。这种设计的初衷是为了保证我们能处理所有的情况,特别是那种会造成十亿美金的空值问题(不太读得懂)。

_ 通配符

当我们不想在匹配的时候列出所有值的时候可以使用Rust提供的一个特殊模式,例如,u8 可以拥有 0 到 255 的有效的值,但是我们只关心 1、3、5 和 7 这几个值,不想列出其它的 0、2、4、6、8、9 一直到 255 的值。那么, 我们不必一个一个列出所有值, 因为可以使用使用特殊的模式 _ 替代:

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

通过将_其放置于其他分支后,_将会匹配所有遗漏的值。()表示啥都不做的意思,所以当匹配到_后,什么也不会发生。

然后,在某些场景下,我们其实只关心某一个值是否存在,此时match就显得过于啰嗦。

if let匹配

有时会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,如果用match来处理就要写成下面这样:

    let v = Some(3u8);
    match v{
        Some(3) => println!("three"),
        _ => (),
    }

我们只想要对 Some(3) 模式进行匹配, 不想处理任何其他 Some<u8> 值或 None 值。但是为了满足match表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上 _ => (),这样会增加不少无用的代码。

杀鸡焉用牛刀,可以用if let的方式来实现:

if let Some(3) = some_u8_value {
    println!("three");
}

这两种匹配对于新手来说,可能有些难以抉择,但是只要记住一点就好:当你只要匹配一个条件,且忽略其他条件时就用if let否则都用match

matches!宏

Rust标准库中提供了一个非常实用的宏:matches!,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果true or false

例如,有一个动态数组,里面存有以下枚举:

enum MyEnum {
    Foo,
    Bar
}

fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}

现在如果想对v进行过滤,只保留类型是MyEnum::Foo的元素,你可能想这么写:

v.iter().filter(|x| x == MyEnum::Foo);

但是,实际上这行代码会报错,因为x的类型是&&MyEnum,无法跟类型MyEnum通过==进行比较(这里不太懂想表达什么,编译器跑了一下报了这个错误)。好在,你可以使用match来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用matches!:

v.iter().filter(|x| matches!(x, MyEnum::Foo));

很简单也很简洁,再来看看更多的例子:

let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));

变量覆盖

无论是是match还是if let,他们都可以在模式匹配时覆盖掉老的值,绑定新的值:

fn main() {
   let age = Some(30);
   println!("在匹配前age是{:?}",age);
   if let Some(age) = age {
       println!("匹配出来的age是{}",age);
   }

   println!("在匹配后age是{:?}",age);
}

cargo run运行后输出如下:

在匹配前age是Some(30)
匹配出来的age是30
在匹配后age是Some(30)

可以看出在if let中,=右边Some(i32)类型的age被左边i32类型的新age覆盖了,该覆盖一直持续到if let语句块的结束。因此第三个println!输出的age依然是Some(i32)类型。

对于match类型也是如此:

fn main() {
   let age = Some(30);
   println!("在匹配前age是{:?}",age);
   match age {
       Some(age) =>  println!("匹配出来的age是{}",age),
       _ => ()
   }
   println!("在匹配后age是{:?}",age);
}

需要注意的是,match中的变量覆盖其实不是那么的容易看出,因此要小心!