smallnewer / bugs

18 stars 4 forks source link

Rust记录之模式匹配 #140

Open smallnewer opened 5 years ago

smallnewer commented 5 years ago

刚开始接触Rust,边学边记录,欢迎指正。

match初识

模式匹配(Pattern matching)在Rust中非常常见,模式可以用在match、let等多个地方,本篇主要涉及到的是match表达式。模式也有很多种,如单值、多个值、范围等,后面会逐一提到。 match可以拿其他语言中的switch来做类比和对比从而加深印象。

模式匹配在Rust中通常出现在变量绑定、match语句中。TODO 那变量绑定怎么算模式匹配 模式匹配是match?还是match中可以用模式匹配

switch和穷尽性检查

match配合多种模式比switch强大许多,不过刚开始接触match,可以拿switch作为类比对象,它们都可以对一个变量的值进行分支处理。

let x = 1;

match x {
    1 => println!("1走这里"),
    _ => println!("其他情况"),
}

不过双方本质不同导致许多行为不同,譬如switchbreak可以打断匹配流程。而match则要求同时只能进入一个分支。

var x = 1;
switch (x) {
    case 1: console.log(1);
    case 2: console.log(2);
    case 3: console.log(3);
}
// 无论x是什么值,都会输出123.必须使用break打断。

举个不恰当的例子,switch有点像下楼梯,只要不停下来可以一直往下走,直到最底部。match则像岔口,只能从中选择匹配到的进入。 而Rust要求match穷尽出所有的可能性(exhaustiveness checking),因此不存在匹配不到的情况。

let x = 1;

match x {
    1 => println!("1走这里"),
    _ => println!("其他情况"),  // 没有这一句是无法编译过的,所有非1的其他int都会走这条分支
}

检查穷尽性可以保证我们不会忘了处理某个值。要么match中声明了所有可能的值,要么使用_来表达其他所有的可能性。 当然也可以使用一个新的变量来代替

let x = 1;

match x {
    1 => println!("1走这里"),
    c => println!("其他情况"),  // 只是会得到一个unused variable: `c`的警告
}

在这里c只在当前分支内有效。

match也是表达式

match里的每一个分支都由val => expression组成,当变量匹配到对应分支的val时(这个val理解为模式更好些),右侧的表达式就会开始执行。 match同样是一个表达式,这意味着它也是有返回值的。它的返回值为对应分支的表达式的返回值。

let x = 5;
let y = match x {
    5 => "5str",
    _ => "_str",
}

在进行类型转换时,用这种写法会很不错。

match和enum

match另一个重要的场景就是匹配枚举的可能值。

enum Dir {
    Left,
    Right,
}

fn test (msg: Dir) {
    match msg {
        Dir::Left => println!("left"),
        Dir::Right => println!("right"),
    }
}

由于使用==对比是一个trait需额外实现,所以上述代码在用if和==来模拟时是无法成功的,必须用match关键字。

模式Patterns

接下来详细看看match里的模式(TODO 事实上模式也可以用在绑定,这里主要看的是match中) 模式是一个很难形象理解的东西,它既可以用在match里,也经常出现在let绑定中。类似的解构可以类比一下。 模式有很多种,需要多写来掌握不同的模式的最佳应用场景。

多重匹配

可以使用|进行多重匹配

let x = 1;
match x {
    1 | 2 => println!("一或二"),
    3 => println!("三"),
    _ => println!("其他"),
}

解构

对于复合数据类型,例如struct,可以在模式匹配中使用解构

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

let origin = Point {x: 0, y: 0};
match origin {
    Point {x, y} => println!("{}, {}", x, y),
}

还可以通过:解构为另外一个名字

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

let origin = Point {x: 0, y: 0};
match origin {
    Point {x: x1, y: y1} => println!("{}, {}", x1, y1),
}

这里还可以..忽略一些不用的值

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

let origin = Point {x: 0, y: 0};
match origin {
    Point {y: y1, ..} => println!("{}", y1),
}

解构可以用在任何复合数据类型(compound data type)。譬如enums、tuple。

忽略绑定

可以使用_来忽略掉不用的变量。在JS中用_和x做变量名并没有本质不同,而在Rust中则实实在在的约定不可用,譬如下面的``被用到时会无法编译。

fn coordinate() -> (i32, i32, i32, i32) {
    1,2,3,4
}

let (x, _, z, _) = coordinate();
println!("{}, {}", x, z);
// println!("{}, {}, {}", x, _, z); // 会编译报错

任何模式匹配中的变量绑定都可以用_忽略,想要去一个大的结构体的部分字段时,这个功能非常有用。 类似的可以用..来忽略多个变量值

enum OptionalTuple {
    Value(i32, i32, i32),
    Missing,
}

let x = OptionalTuple::Value(5, -2, 3);

match x {
    OptionalTuple::Value(..) => println!("并不想要tuple中的值,这是想走到这个这个分支而已"),
    OptionalTuple::Missing => println!("只是想走到这个分支而已"),
}

ref和ref mut

如果想获取一个引用,可以使用ref关键字。(TODO 关于ref将来需要单独总结一篇文章)

let x = 5;
match x {
    ref x1 => println!("这是一个引用,{}", x1),  // x1 类型为&i32,而非i32
}

ref创建了一个不可变引用,如果想用可变引用需要用ref mut

let mut x = 5;
match x {
    ref mut x1 => println!("这是一个引用,{}", x1 + 1), 
}

范围Ranges

可以用...来匹配一个范围(三个点,俩点的是解构时忽略值)

let x = 1;

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

Ranges统一可以用给char

let x = '💅';

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

绑定

可以用@配合ranges做变量绑定,通常用来获取复合数据中的部分数据

#[derive(Debug)]
struct Person {
    name: Option<String>,
}

let name = "Steve".to_string();
let mut x: Option<Person> = Some(Person { name: Some(name) });
match x {
    Some(Person { name: ref a @ Some(_), .. }) => println!("{:?}", a),
    _ => {}
}

使用@绑定变量应该是有开销(TODO 待总结。)

match guards

这个怎么翻译合适… 可以在分支上继续使用if来达到某些效果,有点类似于条件1 && 条件2&&的感觉

enum OptionalInt {
    Value(i32),
    Missing,
}

let x = OptionalInt::Value(5);

match x {
    OptionalInt::Value(i) if i > 5 => println!("大于5的数"),
    OptionalInt::Value(..) => println!("小于等于5的数"),
    OptionalInt::Missing => println!("Missing"),
}

等价于

enum OptionalInt {
    Value(i32),
    Missing,
}

let x = OptionalInt::Value(5);

match x {
    OptionalInt::Value(i)  => {
        if i > 5 {
            println!(">5")
        }else{
            println!("<=5")
        }
    },
    OptionalInt::Missing => println!("No such luck."),
}

你可以想象一下这样的场景该如何书写:输入三个变量,当x为Dir::Left并且z==0且last为Dir::Right的时候,返回"yes",其他返回"no" TODO match和if的性能对比。

if let和while let

match表达式配合模式虽然很强大,但面对一些场景时它的代码就显得不够“少”,为此有了if letwhile let表达式。(这种两个关键字组合的表达式,刚接触的时候怪怪的) 当只需要匹配一个分支的时候,可以使用if let精简代码

let num: Option<i32> = None;

if let Some(_) = num {
    println!("num是Some")
}else{
    println!("num不是Some")
}
let x = 2;
// 需要注意的是,值需要写在左侧,如果写成if let x = 1会成为必命中的分支
// 具体原因还未知晓
if let 1 = x {
    println!("是1")
}else{
    println!("不是1")
}

while let则是处理循环时的某种情况:

let mut arr = vec![0,1,2,3,4,5];
while let Some(val) = arr.pop() {
    println!("{}", val)
}

小结

模式匹配的精髓在于match+patterns,两者缺一不可。它提供了强大的匹配能力,但同时面对一些特殊的场景时,它需要的书写代码可能会更多。 通常会用Rust的宏来解决这些场景,提高代码的简洁性。

TODO ref文章 TODO match和if的性能测试 TODO ..和...的文章 TODO 补充解构在实际项目中的运用。 TODO if let 模式匹配变量的位置在左和右的区别

相关资料

Patterns Match